From 6465d3e439b55de05ed43403724d6f9d12cb210a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sun, 7 Jul 2024 21:11:48 +0200 Subject: [PATCH 01/28] Move to VCPKG. * Add proper CMake packaging for UIBase. * Install pdb to pdb subfolder instead of lib. * Add alias mo2::uibase target for uibase. * Use spdlog from MO2 registry. * Generate translations for UIBase. * Publish release on tag. * Add a few string methods to remove boost dependencies. --- .github/workflows/build.yml | 89 +++- .gitignore | 2 + CMakeLists.txt | 41 +- CMakePresets.json | 63 +++ README.md | 57 ++- cmake/config.cmake.in | 13 + {src => include/uibase}/delayedfilewriter.h | 0 {src => include/uibase}/diagnosisreport.h | 0 {src => include/uibase}/dllimport.h | 0 {src => include/uibase}/errorcodes.h | 0 {src => include/uibase}/eventfilter.h | 0 {src => include/uibase}/exceptions.h | 0 {src => include/uibase}/executableinfo.h | 0 {src => include/uibase}/expanderwidget.h | 0 {src => include/uibase}/filemapping.h | 0 {src => include/uibase}/filesystemutilities.h | 0 {src => include/uibase}/filterwidget.h | 0 {src => include/uibase}/finddialog.h | 0 {src => include/uibase}/formatters.h | 0 {src => include/uibase}/formatters/enums.h | 0 {src => include/uibase}/formatters/qt.h | 0 .../formatters/random_access_containers.h | 0 {src => include/uibase}/formatters/strings.h | 0 .../uibase}/game_features/bsainvalidation.h | 0 .../uibase}/game_features/dataarchives.h | 0 .../uibase}/game_features/game_feature.h | 0 .../uibase}/game_features/gameplugins.h | 0 .../uibase}/game_features/igamefeatures.h | 0 .../uibase}/game_features/localsavegames.h | 2 +- .../uibase}/game_features/moddatachecker.h | 0 .../uibase}/game_features/moddatacontent.h | 0 .../uibase}/game_features/savegameinfo.h | 0 .../uibase}/game_features/scriptextender.h | 0 .../uibase}/game_features/unmanagedmods.h | 0 {src => include/uibase}/guessedvalue.h | 0 {src => include/uibase}/idownloadmanager.h | 0 {src => include/uibase}/ifiletree.h | 0 .../uibase}/iinstallationmanager.h | 0 {src => include/uibase}/imodinterface.h | 0 {src => include/uibase}/imodlist.h | 0 .../uibase}/imodrepositorybridge.h | 0 {src => include/uibase}/imoinfo.h | 2 +- {src => include/uibase}/iplugin.h | 0 {src => include/uibase}/iplugindiagnose.h | 0 {src => include/uibase}/ipluginfilemapper.h | 0 {src => include/uibase}/iplugingame.h | 0 {src => include/uibase}/iplugingamefeatures.h | 0 {src => include/uibase}/iplugininstaller.h | 0 .../uibase}/iplugininstallercustom.h | 0 .../uibase}/iplugininstallersimple.h | 0 {src => include/uibase}/ipluginlist.h | 0 {src => include/uibase}/ipluginmodpage.h | 0 {src => include/uibase}/ipluginpreview.h | 0 {src => include/uibase}/ipluginproxy.h | 0 {src => include/uibase}/iplugintool.h | 0 {src => include/uibase}/iprofile.h | 0 {src => include/uibase}/isavegame.h | 0 {src => include/uibase}/isavegameinfowidget.h | 0 {src => include/uibase}/json.h | 0 {src => include/uibase}/lineeditclear.h | 0 {src => include/uibase}/linklabel.h | 0 {src => include/uibase}/log.h | 4 +- {src => include/uibase}/memoizedlock.h | 0 {src => include/uibase}/moassert.h | 0 .../uibase}/modrepositoryfileinfo.h | 0 {src => include/uibase}/nxmurl.h | 0 {src => include/uibase}/pluginrequirements.h | 0 {src => include/uibase}/pluginsetting.h | 0 {src => include/uibase}/questionboxmemory.h | 0 {src => include/uibase}/registry.h | 0 {src => include/uibase}/report.h | 0 {src => include/uibase}/safewritefile.h | 0 {src => include/uibase}/scopeguard.h | 0 {src => include/uibase}/sortabletreewidget.h | 0 {src => include/uibase}/steamutility.h | 0 include/uibase/strings.h | 16 + {src => include/uibase}/taskprogressmanager.h | 0 {src => include/uibase}/textviewer.h | 0 {src => include/uibase}/tutorabledialog.h | 0 {src => include/uibase}/tutorialcontrol.h | 0 {src => include/uibase}/tutorialmanager.h | 0 {src => include/uibase}/utility.h | 0 {src => include/uibase}/versioninfo.h | 0 {src => include/uibase}/widgetutility.h | 0 src/CMakeLists.txt | 258 ++++++++--- src/SConscript | 52 --- src/log.cpp | 17 +- src/strings.cpp | 46 ++ src/uibase_en.ts | 407 ++++++++++++++++++ src/version.rc | 6 +- tests/CMakeLists.txt | 10 +- tests/cmake/CMakeLists.txt | 9 + tests/cmake/plugin.cpp | 4 + tests/test_formatters.cpp | 2 +- tests/test_ifiletree.cpp | 2 +- tests/test_strings.cpp | 33 ++ vcpkg-configuration.json | 21 + vcpkg.json | 15 + 98 files changed, 1016 insertions(+), 155 deletions(-) create mode 100644 CMakePresets.json create mode 100644 cmake/config.cmake.in rename {src => include/uibase}/delayedfilewriter.h (100%) rename {src => include/uibase}/diagnosisreport.h (100%) rename {src => include/uibase}/dllimport.h (100%) rename {src => include/uibase}/errorcodes.h (100%) rename {src => include/uibase}/eventfilter.h (100%) rename {src => include/uibase}/exceptions.h (100%) rename {src => include/uibase}/executableinfo.h (100%) rename {src => include/uibase}/expanderwidget.h (100%) rename {src => include/uibase}/filemapping.h (100%) rename {src => include/uibase}/filesystemutilities.h (100%) rename {src => include/uibase}/filterwidget.h (100%) rename {src => include/uibase}/finddialog.h (100%) rename {src => include/uibase}/formatters.h (100%) rename {src => include/uibase}/formatters/enums.h (100%) rename {src => include/uibase}/formatters/qt.h (100%) rename {src => include/uibase}/formatters/random_access_containers.h (100%) rename {src => include/uibase}/formatters/strings.h (100%) rename {src => include/uibase}/game_features/bsainvalidation.h (100%) rename {src => include/uibase}/game_features/dataarchives.h (100%) rename {src => include/uibase}/game_features/game_feature.h (100%) rename {src => include/uibase}/game_features/gameplugins.h (100%) rename {src => include/uibase}/game_features/igamefeatures.h (100%) rename {src => include/uibase}/game_features/localsavegames.h (94%) rename {src => include/uibase}/game_features/moddatachecker.h (100%) rename {src => include/uibase}/game_features/moddatacontent.h (100%) rename {src => include/uibase}/game_features/savegameinfo.h (100%) rename {src => include/uibase}/game_features/scriptextender.h (100%) rename {src => include/uibase}/game_features/unmanagedmods.h (100%) rename {src => include/uibase}/guessedvalue.h (100%) rename {src => include/uibase}/idownloadmanager.h (100%) rename {src => include/uibase}/ifiletree.h (100%) rename {src => include/uibase}/iinstallationmanager.h (100%) rename {src => include/uibase}/imodinterface.h (100%) rename {src => include/uibase}/imodlist.h (100%) rename {src => include/uibase}/imodrepositorybridge.h (100%) rename {src => include/uibase}/imoinfo.h (99%) rename {src => include/uibase}/iplugin.h (100%) rename {src => include/uibase}/iplugindiagnose.h (100%) rename {src => include/uibase}/ipluginfilemapper.h (100%) rename {src => include/uibase}/iplugingame.h (100%) rename {src => include/uibase}/iplugingamefeatures.h (100%) rename {src => include/uibase}/iplugininstaller.h (100%) rename {src => include/uibase}/iplugininstallercustom.h (100%) rename {src => include/uibase}/iplugininstallersimple.h (100%) rename {src => include/uibase}/ipluginlist.h (100%) rename {src => include/uibase}/ipluginmodpage.h (100%) rename {src => include/uibase}/ipluginpreview.h (100%) rename {src => include/uibase}/ipluginproxy.h (100%) rename {src => include/uibase}/iplugintool.h (100%) rename {src => include/uibase}/iprofile.h (100%) rename {src => include/uibase}/isavegame.h (100%) rename {src => include/uibase}/isavegameinfowidget.h (100%) rename {src => include/uibase}/json.h (100%) rename {src => include/uibase}/lineeditclear.h (100%) rename {src => include/uibase}/linklabel.h (100%) rename {src => include/uibase}/log.h (98%) rename {src => include/uibase}/memoizedlock.h (100%) rename {src => include/uibase}/moassert.h (100%) rename {src => include/uibase}/modrepositoryfileinfo.h (100%) rename {src => include/uibase}/nxmurl.h (100%) rename {src => include/uibase}/pluginrequirements.h (100%) rename {src => include/uibase}/pluginsetting.h (100%) rename {src => include/uibase}/questionboxmemory.h (100%) rename {src => include/uibase}/registry.h (100%) rename {src => include/uibase}/report.h (100%) rename {src => include/uibase}/safewritefile.h (100%) rename {src => include/uibase}/scopeguard.h (100%) rename {src => include/uibase}/sortabletreewidget.h (100%) rename {src => include/uibase}/steamutility.h (100%) create mode 100644 include/uibase/strings.h rename {src => include/uibase}/taskprogressmanager.h (100%) rename {src => include/uibase}/textviewer.h (100%) rename {src => include/uibase}/tutorabledialog.h (100%) rename {src => include/uibase}/tutorialcontrol.h (100%) rename {src => include/uibase}/tutorialmanager.h (100%) rename {src => include/uibase}/utility.h (100%) rename {src => include/uibase}/versioninfo.h (100%) rename {src => include/uibase}/widgetutility.h (100%) delete mode 100644 src/SConscript create mode 100644 src/strings.cpp create mode 100644 src/uibase_en.ts create mode 100644 tests/cmake/CMakeLists.txt create mode 100644 tests/cmake/plugin.cpp create mode 100644 tests/test_strings.cpp create mode 100644 vcpkg-configuration.json create mode 100644 vcpkg.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27b73f02..893fa507 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,25 +3,92 @@ name: Build UIBase on: push: branches: master + tags: + - "*" pull_request: types: [opened, synchronize, reopened] +env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + jobs: build: runs-on: windows-2022 steps: - - name: Build UI Base - id: build-uibase - uses: ModOrganizer2/build-with-mob-action@master + # https://learn.microsoft.com/en-us/vcpkg/consume/binary-caching-github-actions-cache + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 with: - mo2-third-parties: gtest spdlog boost - mo2-dependencies: cmake_common - mo2-cmake-command: -DUIBASE_TESTS=1 .. + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: 6.7.0 + modules: + cache: true + + - uses: actions/checkout@v4 + - uses: lukka/run-vcpkg@v11 - - name: Build UI Base Tests - run: cmake --build vsbuild --config RelWithDebInfo -j4 --target uibase-tests - working-directory: ${{ steps.build-uibase.outputs.working-directory }} + - name: Configure UI Base build + shell: pwsh + run: | + cmake --preset vs2022-windows ` + "-DCMAKE_PREFIX_PATH=${env:QT_ROOT_DIR}\msvc2019_64" ` + "-DCMAKE_INSTALL_PREFIX=install" -DUIBASE_TESTING=ON + + # build both Debug and RelWithDebInfo for package + - name: Build UI Base + run: | + cmake --build vsbuild --config Debug --target uibase-tests --verbose + cmake --build vsbuild --config RelWithDebInfo --target uibase-tests --verbose - name: Test UI Base - run: ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure - working-directory: ${{ steps.build-uibase.outputs.working-directory }} + run: | + ctest --test-dir vsbuild -C Debug --output-on-failure + ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure + + - name: Install UI Base + run: | + cmake --install vsbuild --config Debug + cmake --install vsbuild --config RelWithDebInfo + + # this tests that UI Base can be properly used as a CMake package + - name: Test UI Base package + run: | + cmake -B build . "-DCMAKE_PREFIX_PATH=${env:QT_ROOT_DIR}\msvc2019_64;..\..\install\lib\cmake\" + cmake --build build --config Debug + cmake --build build --config Release + cmake --build build --config RelWithDebInfo + working-directory: tests/cmake + + - name: Upload UI Base artifact + uses: actions/upload-artifact@master + with: + name: uibase + path: ./install + + publish: + if: github.ref_type == 'tag' + needs: build + runs-on: windows-2022 + permissions: + contents: write + steps: + - name: Download Artifact + uses: actions/download-artifact@master + with: + name: uibase + path: ./install + + - name: Create UI Base archive + run: 7z a uibase_${{ github.ref_name }}.7z ./install/* + + - name: Publish Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release create --draft=false --notes="${{ github.ref_name }}" "${{ github.ref_name }}" ./uibase_${{ github.ref_name }}.7z diff --git a/.gitignore b/.gitignore index 1dfac70c..bcdbd6a5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ CMakeLists.txt.user /*std*.log /*build /src/uibasetests_en.ts +/install +/tests/cmake/build diff --git a/CMakeLists.txt b/CMakeLists.txt index 800fa66d..ec4792c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,44 @@ cmake_minimum_required(VERSION 3.16) +include(CMakePackageConfigHelpers) + project(uibase) -if(DEFINED DEPENDENCIES_DIR) - include(${DEPENDENCIES_DIR}/modorganizer_super/cmake_common/mo2.cmake) -else() - include(${CMAKE_CURRENT_LIST_DIR}/../cmake_common/mo2.cmake) -endif() +find_package(mo2-cmake CONFIG REQUIRED) add_subdirectory(src) -set(UIBASE_TESTS ${UIBASE_TESTS} CACHE BOOL "build tests for uibase") -if (UIBASE_TESTS) +mo2_set_project_to_run_from_install(uibase EXECUTABLE ${CMAKE_INSTALL_PREFIX}/bin/ModOrganizer.exe) +set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT uibase) + +configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config.cmake" + INSTALL_DESTINATION "lib/cmake/mo2-uibase" + NO_SET_AND_CHECK_MACRO + NO_CHECK_REQUIRED_COMPONENTS_MACRO +) + +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/version.rc" uibase_version) +string(REGEX MATCH "#define VER_FILEVERSION\\s*([0-9]+),([0-9]+),([0-9]+)" _ ${uibase_version}) +set(uibase_version_major ${CMAKE_MATCH_1}) +set(uibase_version_minor ${CMAKE_MATCH_2}) +set(uibase_version_patch ${CMAKE_MATCH_3}) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config-version.cmake" + VERSION "${uibase_version_major}.${uibase_version_minor}.${uibase_version_patch}" + COMPATIBILITY AnyNewerVersion + ARCH_INDEPENDENT +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config-version.cmake + DESTINATION lib/cmake/mo2-uibase +) + +set(UIBASE_TESTING ${UIBASE_TESTING} CACHE BOOL "build tests for uibase") +if (UIBASE_TESTING) enable_testing() add_subdirectory(tests) endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..0c06d1a3 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,63 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "testing" + } + }, + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/README.md b/README.md index 97468b30..ba220be2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,56 @@ -[![Build status](https://ci.appveyor.com/api/projects/status/g8c6tujn0pne6tsk?svg=true)](https://ci.appveyor.com/project/Modorganizer2/modorganizer-uibase) - # modorganizer-uibase + +[![Build status](https://github.com/ModOrganizer2/modorganizer-uibase/actions/workflows/build.yml/badge.svg?branch=dev/vcpkg)](https://github.com/ModOrganizer2/modorganizer-uibase/actions) +[![Lint status]](https://github.com/ModOrganizer2/modorganizer-uibase/actions/workflows/linting.yml/badge.svg?branch=dev/vcpkg)]() + +## How to build? + +```pwsh +# set to the appropriate path for Qt +$env:QT_ROOT = "C:\Qt\6.7.0\msvc2019_64" + +# set to the appropriate path for VCPKG +$env:VCPKG_ROOT = "C:\vcpkg" + +cmake --preset vs2022-windows "-DCMAKE_PREFIX_PATH=$env:QT_ROOT" ` + -DCMAKE_INSTALL_PREFIX=install ` + -DUIBASE_TESTING=ON + +# build uibase +cmake --build vsbuild --config RelWithDebInfo + +# install uibase +cmake --install vsbuild --config RelWithDebInfo + +# test uibase +ctest --test-dir vsbuild -C RelWithDebInfo --output-on-failure +``` + +Check [`CMakePresets.json`](CMakePresets.json) for some predefined values. Extra options +include: + +- `UIBASE_TESTING` - if specified, build tests for UIBase, requires the VCPKG `testing` + feature to be enabled (enabled in the preset). + +## How to use? + +### As a VCPKG dependency + +**Not implemented yet.** + +### As a CMake target + +Once the CMake targets for `uibase` are generated (see _How to build?_), you can include +`mo2::uibase` in your project: + +1. Add `install/lib` to your `CMAKE_PREFIX_PATH` (replace `install` by the install + location specified during build). +2. Use `uibase` in your `CMakeLists.txt`: + +```cmake +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(myplugin SHARED) + +target_link_libraries(myplugin PRIVATE mo2::uibase) +``` diff --git a/cmake/config.cmake.in b/cmake/config.cmake.in new file mode 100644 index 00000000..d6d78995 --- /dev/null +++ b/cmake/config.cmake.in @@ -0,0 +1,13 @@ +@PACKAGE_INIT@ + +set(_UIBASE_PREFIX_DIR ${PACKAGE_PREFIX_DIR}) + +find_package(Qt6 CONFIG REQUIRED COMPONENTS Network QuickWidgets Widgets) + +include ( "${CMAKE_CURRENT_LIST_DIR}/mo2-uibase-targets.cmake" ) + + +if (MO2_CMAKE_DEPRECATED_UIBASE_INCLUDE) + target_include_directories(mo2::uibase INTERFACE + ${_UIBASE_PREFIX_DIR}/include/uibase ${_UIBASE_PREFIX_DIR}/include/uibase/game_features) +endif() diff --git a/src/delayedfilewriter.h b/include/uibase/delayedfilewriter.h similarity index 100% rename from src/delayedfilewriter.h rename to include/uibase/delayedfilewriter.h diff --git a/src/diagnosisreport.h b/include/uibase/diagnosisreport.h similarity index 100% rename from src/diagnosisreport.h rename to include/uibase/diagnosisreport.h diff --git a/src/dllimport.h b/include/uibase/dllimport.h similarity index 100% rename from src/dllimport.h rename to include/uibase/dllimport.h diff --git a/src/errorcodes.h b/include/uibase/errorcodes.h similarity index 100% rename from src/errorcodes.h rename to include/uibase/errorcodes.h diff --git a/src/eventfilter.h b/include/uibase/eventfilter.h similarity index 100% rename from src/eventfilter.h rename to include/uibase/eventfilter.h diff --git a/src/exceptions.h b/include/uibase/exceptions.h similarity index 100% rename from src/exceptions.h rename to include/uibase/exceptions.h diff --git a/src/executableinfo.h b/include/uibase/executableinfo.h similarity index 100% rename from src/executableinfo.h rename to include/uibase/executableinfo.h diff --git a/src/expanderwidget.h b/include/uibase/expanderwidget.h similarity index 100% rename from src/expanderwidget.h rename to include/uibase/expanderwidget.h diff --git a/src/filemapping.h b/include/uibase/filemapping.h similarity index 100% rename from src/filemapping.h rename to include/uibase/filemapping.h diff --git a/src/filesystemutilities.h b/include/uibase/filesystemutilities.h similarity index 100% rename from src/filesystemutilities.h rename to include/uibase/filesystemutilities.h diff --git a/src/filterwidget.h b/include/uibase/filterwidget.h similarity index 100% rename from src/filterwidget.h rename to include/uibase/filterwidget.h diff --git a/src/finddialog.h b/include/uibase/finddialog.h similarity index 100% rename from src/finddialog.h rename to include/uibase/finddialog.h diff --git a/src/formatters.h b/include/uibase/formatters.h similarity index 100% rename from src/formatters.h rename to include/uibase/formatters.h diff --git a/src/formatters/enums.h b/include/uibase/formatters/enums.h similarity index 100% rename from src/formatters/enums.h rename to include/uibase/formatters/enums.h diff --git a/src/formatters/qt.h b/include/uibase/formatters/qt.h similarity index 100% rename from src/formatters/qt.h rename to include/uibase/formatters/qt.h diff --git a/src/formatters/random_access_containers.h b/include/uibase/formatters/random_access_containers.h similarity index 100% rename from src/formatters/random_access_containers.h rename to include/uibase/formatters/random_access_containers.h diff --git a/src/formatters/strings.h b/include/uibase/formatters/strings.h similarity index 100% rename from src/formatters/strings.h rename to include/uibase/formatters/strings.h diff --git a/src/game_features/bsainvalidation.h b/include/uibase/game_features/bsainvalidation.h similarity index 100% rename from src/game_features/bsainvalidation.h rename to include/uibase/game_features/bsainvalidation.h diff --git a/src/game_features/dataarchives.h b/include/uibase/game_features/dataarchives.h similarity index 100% rename from src/game_features/dataarchives.h rename to include/uibase/game_features/dataarchives.h diff --git a/src/game_features/game_feature.h b/include/uibase/game_features/game_feature.h similarity index 100% rename from src/game_features/game_feature.h rename to include/uibase/game_features/game_feature.h diff --git a/src/game_features/gameplugins.h b/include/uibase/game_features/gameplugins.h similarity index 100% rename from src/game_features/gameplugins.h rename to include/uibase/game_features/gameplugins.h diff --git a/src/game_features/igamefeatures.h b/include/uibase/game_features/igamefeatures.h similarity index 100% rename from src/game_features/igamefeatures.h rename to include/uibase/game_features/igamefeatures.h diff --git a/src/game_features/localsavegames.h b/include/uibase/game_features/localsavegames.h similarity index 94% rename from src/game_features/localsavegames.h rename to include/uibase/game_features/localsavegames.h index 2db6a4e2..f425431e 100644 --- a/src/game_features/localsavegames.h +++ b/include/uibase/game_features/localsavegames.h @@ -3,8 +3,8 @@ #include +#include "../filemapping.h" #include "./game_feature.h" -#include "filemapping.h" namespace MOBase { diff --git a/src/game_features/moddatachecker.h b/include/uibase/game_features/moddatachecker.h similarity index 100% rename from src/game_features/moddatachecker.h rename to include/uibase/game_features/moddatachecker.h diff --git a/src/game_features/moddatacontent.h b/include/uibase/game_features/moddatacontent.h similarity index 100% rename from src/game_features/moddatacontent.h rename to include/uibase/game_features/moddatacontent.h diff --git a/src/game_features/savegameinfo.h b/include/uibase/game_features/savegameinfo.h similarity index 100% rename from src/game_features/savegameinfo.h rename to include/uibase/game_features/savegameinfo.h diff --git a/src/game_features/scriptextender.h b/include/uibase/game_features/scriptextender.h similarity index 100% rename from src/game_features/scriptextender.h rename to include/uibase/game_features/scriptextender.h diff --git a/src/game_features/unmanagedmods.h b/include/uibase/game_features/unmanagedmods.h similarity index 100% rename from src/game_features/unmanagedmods.h rename to include/uibase/game_features/unmanagedmods.h diff --git a/src/guessedvalue.h b/include/uibase/guessedvalue.h similarity index 100% rename from src/guessedvalue.h rename to include/uibase/guessedvalue.h diff --git a/src/idownloadmanager.h b/include/uibase/idownloadmanager.h similarity index 100% rename from src/idownloadmanager.h rename to include/uibase/idownloadmanager.h diff --git a/src/ifiletree.h b/include/uibase/ifiletree.h similarity index 100% rename from src/ifiletree.h rename to include/uibase/ifiletree.h diff --git a/src/iinstallationmanager.h b/include/uibase/iinstallationmanager.h similarity index 100% rename from src/iinstallationmanager.h rename to include/uibase/iinstallationmanager.h diff --git a/src/imodinterface.h b/include/uibase/imodinterface.h similarity index 100% rename from src/imodinterface.h rename to include/uibase/imodinterface.h diff --git a/src/imodlist.h b/include/uibase/imodlist.h similarity index 100% rename from src/imodlist.h rename to include/uibase/imodlist.h diff --git a/src/imodrepositorybridge.h b/include/uibase/imodrepositorybridge.h similarity index 100% rename from src/imodrepositorybridge.h rename to include/uibase/imodrepositorybridge.h diff --git a/src/imoinfo.h b/include/uibase/imoinfo.h similarity index 99% rename from src/imoinfo.h rename to include/uibase/imoinfo.h index 407a379a..e4f072a8 100644 --- a/src/imoinfo.h +++ b/include/uibase/imoinfo.h @@ -30,7 +30,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include #include -#include "game_feature.h" +#include "game_features/game_feature.h" #include "guessedvalue.h" #include "imodlist.h" #include "iprofile.h" diff --git a/src/iplugin.h b/include/uibase/iplugin.h similarity index 100% rename from src/iplugin.h rename to include/uibase/iplugin.h diff --git a/src/iplugindiagnose.h b/include/uibase/iplugindiagnose.h similarity index 100% rename from src/iplugindiagnose.h rename to include/uibase/iplugindiagnose.h diff --git a/src/ipluginfilemapper.h b/include/uibase/ipluginfilemapper.h similarity index 100% rename from src/ipluginfilemapper.h rename to include/uibase/ipluginfilemapper.h diff --git a/src/iplugingame.h b/include/uibase/iplugingame.h similarity index 100% rename from src/iplugingame.h rename to include/uibase/iplugingame.h diff --git a/src/iplugingamefeatures.h b/include/uibase/iplugingamefeatures.h similarity index 100% rename from src/iplugingamefeatures.h rename to include/uibase/iplugingamefeatures.h diff --git a/src/iplugininstaller.h b/include/uibase/iplugininstaller.h similarity index 100% rename from src/iplugininstaller.h rename to include/uibase/iplugininstaller.h diff --git a/src/iplugininstallercustom.h b/include/uibase/iplugininstallercustom.h similarity index 100% rename from src/iplugininstallercustom.h rename to include/uibase/iplugininstallercustom.h diff --git a/src/iplugininstallersimple.h b/include/uibase/iplugininstallersimple.h similarity index 100% rename from src/iplugininstallersimple.h rename to include/uibase/iplugininstallersimple.h diff --git a/src/ipluginlist.h b/include/uibase/ipluginlist.h similarity index 100% rename from src/ipluginlist.h rename to include/uibase/ipluginlist.h diff --git a/src/ipluginmodpage.h b/include/uibase/ipluginmodpage.h similarity index 100% rename from src/ipluginmodpage.h rename to include/uibase/ipluginmodpage.h diff --git a/src/ipluginpreview.h b/include/uibase/ipluginpreview.h similarity index 100% rename from src/ipluginpreview.h rename to include/uibase/ipluginpreview.h diff --git a/src/ipluginproxy.h b/include/uibase/ipluginproxy.h similarity index 100% rename from src/ipluginproxy.h rename to include/uibase/ipluginproxy.h diff --git a/src/iplugintool.h b/include/uibase/iplugintool.h similarity index 100% rename from src/iplugintool.h rename to include/uibase/iplugintool.h diff --git a/src/iprofile.h b/include/uibase/iprofile.h similarity index 100% rename from src/iprofile.h rename to include/uibase/iprofile.h diff --git a/src/isavegame.h b/include/uibase/isavegame.h similarity index 100% rename from src/isavegame.h rename to include/uibase/isavegame.h diff --git a/src/isavegameinfowidget.h b/include/uibase/isavegameinfowidget.h similarity index 100% rename from src/isavegameinfowidget.h rename to include/uibase/isavegameinfowidget.h diff --git a/src/json.h b/include/uibase/json.h similarity index 100% rename from src/json.h rename to include/uibase/json.h diff --git a/src/lineeditclear.h b/include/uibase/lineeditclear.h similarity index 100% rename from src/lineeditclear.h rename to include/uibase/lineeditclear.h diff --git a/src/linklabel.h b/include/uibase/linklabel.h similarity index 100% rename from src/linklabel.h rename to include/uibase/linklabel.h diff --git a/src/log.h b/include/uibase/log.h similarity index 98% rename from src/log.h rename to include/uibase/log.h index d92ef4dd..460c6a52 100644 --- a/src/log.h +++ b/include/uibase/log.h @@ -16,6 +16,7 @@ #include "dllimport.h" #include "formatters.h" +#include "strings.h" namespace spdlog { @@ -62,9 +63,6 @@ concept RuntimeFormatString = requires(F&& f, Args&&... args) { void QDLLEXPORT doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept; -void QDLLEXPORT ireplace_all(std::string& input, std::string const& search, - std::string const& replace) noexcept; - template void doLog(spdlog::logger& logger, Levels lv, const std::vector bl, diff --git a/src/memoizedlock.h b/include/uibase/memoizedlock.h similarity index 100% rename from src/memoizedlock.h rename to include/uibase/memoizedlock.h diff --git a/src/moassert.h b/include/uibase/moassert.h similarity index 100% rename from src/moassert.h rename to include/uibase/moassert.h diff --git a/src/modrepositoryfileinfo.h b/include/uibase/modrepositoryfileinfo.h similarity index 100% rename from src/modrepositoryfileinfo.h rename to include/uibase/modrepositoryfileinfo.h diff --git a/src/nxmurl.h b/include/uibase/nxmurl.h similarity index 100% rename from src/nxmurl.h rename to include/uibase/nxmurl.h diff --git a/src/pluginrequirements.h b/include/uibase/pluginrequirements.h similarity index 100% rename from src/pluginrequirements.h rename to include/uibase/pluginrequirements.h diff --git a/src/pluginsetting.h b/include/uibase/pluginsetting.h similarity index 100% rename from src/pluginsetting.h rename to include/uibase/pluginsetting.h diff --git a/src/questionboxmemory.h b/include/uibase/questionboxmemory.h similarity index 100% rename from src/questionboxmemory.h rename to include/uibase/questionboxmemory.h diff --git a/src/registry.h b/include/uibase/registry.h similarity index 100% rename from src/registry.h rename to include/uibase/registry.h diff --git a/src/report.h b/include/uibase/report.h similarity index 100% rename from src/report.h rename to include/uibase/report.h diff --git a/src/safewritefile.h b/include/uibase/safewritefile.h similarity index 100% rename from src/safewritefile.h rename to include/uibase/safewritefile.h diff --git a/src/scopeguard.h b/include/uibase/scopeguard.h similarity index 100% rename from src/scopeguard.h rename to include/uibase/scopeguard.h diff --git a/src/sortabletreewidget.h b/include/uibase/sortabletreewidget.h similarity index 100% rename from src/sortabletreewidget.h rename to include/uibase/sortabletreewidget.h diff --git a/src/steamutility.h b/include/uibase/steamutility.h similarity index 100% rename from src/steamutility.h rename to include/uibase/steamutility.h diff --git a/include/uibase/strings.h b/include/uibase/strings.h new file mode 100644 index 00000000..e0d76a33 --- /dev/null +++ b/include/uibase/strings.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include "dllimport.h" + +namespace MOBase +{ + +QDLLEXPORT void ireplace_all(std::string& input, std::string_view search, + std::string_view replace) noexcept; + +QDLLEXPORT bool iequals(std::string_view lhs, std::string_view rhs); + +} // namespace MOBase diff --git a/src/taskprogressmanager.h b/include/uibase/taskprogressmanager.h similarity index 100% rename from src/taskprogressmanager.h rename to include/uibase/taskprogressmanager.h diff --git a/src/textviewer.h b/include/uibase/textviewer.h similarity index 100% rename from src/textviewer.h rename to include/uibase/textviewer.h diff --git a/src/tutorabledialog.h b/include/uibase/tutorabledialog.h similarity index 100% rename from src/tutorabledialog.h rename to include/uibase/tutorabledialog.h diff --git a/src/tutorialcontrol.h b/include/uibase/tutorialcontrol.h similarity index 100% rename from src/tutorialcontrol.h rename to include/uibase/tutorialcontrol.h diff --git a/src/tutorialmanager.h b/include/uibase/tutorialmanager.h similarity index 100% rename from src/tutorialmanager.h rename to include/uibase/tutorialmanager.h diff --git a/src/utility.h b/include/uibase/utility.h similarity index 100% rename from src/utility.h rename to include/uibase/utility.h diff --git a/src/versioninfo.h b/include/uibase/versioninfo.h similarity index 100% rename from src/versioninfo.h rename to include/uibase/versioninfo.h diff --git a/src/widgetutility.h b/include/uibase/widgetutility.h similarity index 100% rename from src/widgetutility.h rename to include/uibase/widgetutility.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6384912a..5b096a67 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,72 +1,214 @@ cmake_minimum_required(VERSION 3.16) +find_package(Qt6 CONFIG REQUIRED COMPONENTS Network Qml Quick QuickWidgets Widgets) +find_package(spdlog CONFIG REQUIRED) + +set(root_headers + ../include/uibase/delayedfilewriter.h + ../include/uibase/diagnosisreport.h + ../include/uibase/dllimport.h + ../include/uibase/errorcodes.h + ../include/uibase/eventfilter.h + ../include/uibase/exceptions.h + ../include/uibase/executableinfo.h + ../include/uibase/filemapping.h + ../include/uibase/filesystemutilities.h + ../include/uibase/guessedvalue.h + ../include/uibase/idownloadmanager.h + ../include/uibase/json.h + ../include/uibase/log.h + ../include/uibase/memoizedlock.h + ../include/uibase/moassert.h + ../include/uibase/modrepositoryfileinfo.h + ../include/uibase/nxmurl.h + ../include/uibase/pluginrequirements.h + ../include/uibase/pluginsetting.h + ../include/uibase/registry.h + ../include/uibase/report.h + ../include/uibase/safewritefile.h + ../include/uibase/scopeguard.h + ../include/uibase/steamutility.h + ../include/uibase/strings.h + ../include/uibase/utility.h + ../include/uibase/versioninfo.h +) +set(interface_headers + ../include/uibase/ifiletree.h + ../include/uibase/iinstallationmanager.h + ../include/uibase/imodinterface.h + ../include/uibase/imodlist.h + ../include/uibase/imodrepositorybridge.h + ../include/uibase/imoinfo.h + ../include/uibase/iplugin.h + ../include/uibase/iplugindiagnose.h + ../include/uibase/ipluginfilemapper.h + ../include/uibase/iplugingame.h + ../include/uibase/iplugingamefeatures.h + ../include/uibase/iplugininstaller.h + ../include/uibase/iplugininstallercustom.h + ../include/uibase/iplugininstallersimple.h + ../include/uibase/ipluginlist.h + ../include/uibase/ipluginmodpage.h + ../include/uibase/ipluginpreview.h + ../include/uibase/ipluginproxy.h + ../include/uibase/iplugintool.h + ../include/uibase/iprofile.h + ../include/uibase/isavegame.h + ../include/uibase/isavegameinfowidget.h +) +set(tutorial_headers + ../include/uibase/tutorabledialog.h + ../include/uibase/tutorialcontrol.h + ../include/uibase/tutorialmanager.h +) +set(widget_headers + ../include/uibase/expanderwidget.h + ../include/uibase/filterwidget.h + ../include/uibase/finddialog.h + ../include/uibase/lineeditclear.h + ../include/uibase/linklabel.h + ../include/uibase/questionboxmemory.h + ../include/uibase/sortabletreewidget.h + ../include/uibase/taskprogressmanager.h + ../include/uibase/textviewer.h + ../include/uibase/widgetutility.h +) +set(game_features_header + ../include/uibase/game_features/bsainvalidation.h + ../include/uibase/game_features/dataarchives.h + ../include/uibase/game_features/game_feature.h + ../include/uibase/game_features/gameplugins.h + ../include/uibase/game_features/igamefeatures.h + ../include/uibase/game_features/localsavegames.h + ../include/uibase/game_features/moddatachecker.h + ../include/uibase/game_features/moddatacontent.h + ../include/uibase/game_features/savegameinfo.h + ../include/uibase/game_features/scriptextender.h + ../include/uibase/game_features/unmanagedmods.h +) +set(formatters_header + ../include/uibase/formatters/enums.h + ../include/uibase/formatters/qt.h + ../include/uibase/formatters/random_access_containers.h + ../include/uibase/formatters/strings.h + ../include/uibase/formatters.h +) + add_library(uibase SHARED) -mo2_configure_uibase(uibase - WARNINGS ON - EXTERNAL_WARNINGS ON - TRANSLATIONS OFF - PUBLIC_DEPENDS Qt::Widgets Qt::Network Qt::QuickWidgets - PRIVATE_DEPENDS boost boost::thread Qt::Qml Qt::Quick spdlog) -target_compile_definitions(uibase PRIVATE -DUIBASE_EXPORT) -mo2_install_target(uibase) - -mo2_add_filter(NAME src/interfaces GROUPS - ifiletree - imoinfo - installationtester - iplugin - iplugindiagnose - ipluginfilemapper - iplugingame - iplugingamefeatures - iplugininstaller - iplugininstallercustom - iplugininstallersimple - ipluginlist - ipluginmodpage - ipluginpreview - ipluginproxy - iplugintool - iprofile - isavegame - isavegameinfowidget - iinstallationmanager - imodinterface - imodlist - imodrepositorybridge +mo2_configure_target(uibase NO_SOURCES WARNINGS ON EXTERNAL_WARNINGS ON) +mo2_default_source_group() + +mo2_target_sources(uibase + FOLDER src + PRIVATE + ${root_headers} + delayedfilewriter.cpp + diagnosisreport.cpp + errorcodes.cpp + eventfilter.cpp + executableinfo.cpp + filesystemutilities.cpp + guessedvalue.cpp + json.cpp + log.cpp + modrepositoryfileinfo.cpp + nxmurl.cpp + pch.cpp + pluginrequirements.cpp + pluginsetting.cpp + registry.cpp + report.cpp + safewritefile.cpp + scopeguard.cpp + steamutility.cpp + strings.cpp + utility.cpp + versioninfo.cpp ) -mo2_add_filter(NAME src/tutorials GROUPS - tutorabledialog - tutorialcontrol - tutorialmanager +mo2_target_sources(uibase + FOLDER src/interfaces + PRIVATE + ${interface_headers} + ifiletree.cpp + imodrepositorybridge.cpp + imoinfo.cpp + iplugininstaller.cpp ) -mo2_add_filter(NAME src/widgets GROUPS - finddialog - lineeditclear - questionboxmemory - sortabletreewidget - taskprogressmanager - textviewer - expanderwidget - filterwidget - linklabel - widgetutility +mo2_target_sources(uibase + FOLDER src/tutorials + PRIVATE + ${tutorial_headers} + tutorabledialog.cpp + tutorialcontrol.cpp + tutorialmanager.cpp ) -get_target_property(game_features uibase SOURCES) -list(FILTER game_features INCLUDE REGEX ".*game_features.*") -list(TRANSFORM game_features REPLACE ".*game_features[/\\](.*)[.]h" "game_features/\\1") +mo2_target_sources(uibase + FOLDER src/widgets + PRIVATE + ${widget_headers} + expanderwidget.cpp + finddialog.cpp + lineeditclear.cpp + linklabel.cpp + questionboxmemory.cpp + sortabletreewidget.cpp + taskprogressmanager.cpp + textviewer.cpp + widgetutility.cpp + filterwidget.cpp +) -mo2_add_filter(NAME src/game_features GROUPS - ${game_features} +mo2_target_sources(uibase FOLDER src/formatters PRIVATE ${formatters_header}) +mo2_target_sources(uibase FOLDER src/game_features PRIVATE ${game_features_header}) + +target_sources(uibase + PRIVATE + finddialog.ui + questionboxmemory.ui + taskdialog.ui + textviewer.ui + + version.rc + + ${root_headers} + pch.h + + PUBLIC + FILE_SET HEADERS + BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include + FILES + ${root_headers} + ${interface_headers} + ${tutorial_headers} + ${widget_headers} + ${game_features_header} + ${formatters_header} ) -get_target_property(formatters uibase SOURCES) -list(FILTER formatters INCLUDE REGEX ".*formatters.*") -list(TRANSFORM formatters REPLACE ".*formatters[/\\](.*)[.]h" "formatters/\\1") +set_target_properties(uibase PROPERTIES DEBUG_POSTFIX d) + +# TODO: remove this after fixing UIBase #include<> directives +target_include_directories(uibase PRIVATE + $ + $ +) + +add_library(mo2::uibase ALIAS uibase) + +target_compile_definitions(uibase PRIVATE -DUIBASE_EXPORT SPDLOG_USE_STD_FORMAT) + +target_link_libraries(uibase + PUBLIC Qt6::Widgets Qt6::Network Qt6::QuickWidgets + PRIVATE spdlog::spdlog_header_only Qt6::Qml Qt6::Quick Version) -mo2_add_filter(NAME src/formatters GROUPS - ${formatters} +# installation +install(TARGETS uibase EXPORT uibaseTargets FILE_SET HEADERS) +install(FILES $ DESTINATION pdb) +install(EXPORT uibaseTargets + FILE mo2-uibase-targets.cmake + NAMESPACE mo2:: + DESTINATION lib/cmake/mo2-uibase ) diff --git a/src/SConscript b/src/SConscript deleted file mode 100644 index 2d95af1c..00000000 --- a/src/SConscript +++ /dev/null @@ -1,52 +0,0 @@ -Import('qt_env') - -env = qt_env.Clone() - -modules = [ - 'Script', - 'Core', - 'Gui', - 'Declarative' -] - -if env['QT_MAJOR_VERSION'] > 4: - modules += [ - 'Widgets', - 'Qml', - 'QuickWidgets', - 'WinExtras' - ] - -env.EnableQtModules(*modules) - -env.Uic(Glob('*.ui')) - -env.AppendUnique(LIBS = ['user32', 'shell32', 'ole32']) - -# We have to 'persuade' moc to generate certain other targets and inject them -# into the list of cpps -targets = env.AddExtraMoc(env.Glob('*.h')) - -# Note the order of this is important, or you can pick up the wrong report.h... -env.AppendUnique(CPPPATH = [ - '.', # Why is this necessary? - '${BOOSTPATH}', -]) - -env.AppendUnique(CPPDEFINES = [ - 'UIBASE_LIBRARY', - 'UIBASE_EXPORT' -]) - -#CONFIG(debug, debug|release) { -#} else { -# QMAKE_CXXFLAGS += /Zi /GL -# QMAKE_LFLAGS += /DEBUG /LTCG /LARGEADDRESSAWARE /OPT:REF /OPT:ICF -#} - -lib = env.SharedLibrary('uibase', env.Glob('*.cpp') + targets) - -env.InstallModule(lib) - -res = env['QT_USED_MODULES'] -Return('res') diff --git a/src/log.cpp b/src/log.cpp index 5aac373d..2b5cd173 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -3,10 +3,8 @@ #include "utility.h" #include -#pragma warning(push) -#pragma warning(disable : 4668) -#include -#pragma warning(pop) +#include +#include #pragma warning(push) #pragma warning(disable : 4365) @@ -261,7 +259,7 @@ void Logger::addToBlacklist(const std::string& filter, const std::string& replac bool present = false; for (BlacklistEntry& e : m_conf.blacklist) { - if (boost::algorithm::iequals(e.filter, filter)) { + if (iequals(e.filter, filter)) { e.replacement = replacement; present = true; break; @@ -280,7 +278,7 @@ void Logger::removeFromBlacklist(const std::string& filter) } for (auto it = m_conf.blacklist.begin(); it != m_conf.blacklist.end();) { - if (boost::algorithm::iequals(it->filter, filter)) { + if (iequals(it->filter, filter)) { it = m_conf.blacklist.erase(it); } else { ++it; @@ -395,11 +393,4 @@ void doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept } } -void ireplace_all(std::string& input, std::string const& search, - std::string const& replace) noexcept -{ - // call boost here to avoid bringing the boost include in the header - boost::algorithm::ireplace_all(input, search, replace); -} - } // namespace MOBase::log::details diff --git a/src/strings.cpp b/src/strings.cpp new file mode 100644 index 00000000..ef84fb04 --- /dev/null +++ b/src/strings.cpp @@ -0,0 +1,46 @@ +#include "strings.h" + +#include +#include + +namespace MOBase +{ + +// this is strongly inspired from boost +class is_iequal +{ + std::locale m_loc; + +public: + is_iequal(const std::locale& loc = std::locale()) : m_loc{loc} {} + + template + bool operator()(const T1& Arg1, const T2& Arg2) const + { + return std::toupper(Arg1, m_loc) == std::toupper(Arg2, m_loc); + } +}; + +bool iequals(std::string_view lhs, std::string_view rhs) +{ + return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), is_iequal()); +} + +void ireplace_all(std::string& input, std::string_view search, + std::string_view replace) noexcept +{ + const auto search_length = static_cast(search.size()); + const auto replace_length = replace.size(); + auto it = input.begin(); + while (input.end() - it >= search_length) { + const auto search_end = it + search_length; + if (iequals(std::string_view(it, search_end), search)) { + input.replace(it, search_end, replace); + it += static_cast(replace_length); + } else { + ++it; + } + } +} + +} // namespace MOBase diff --git a/src/uibase_en.ts b/src/uibase_en.ts new file mode 100644 index 00000000..7f02320f --- /dev/null +++ b/src/uibase_en.ts @@ -0,0 +1,407 @@ + + + + + FilterWidget + + + Filter options + + + + + Use regular expressions + + + + + Use regular expressions in filters + + + + + Case sensitive + + + + + Make regular expressions case sensitive (/i) + leave "(/i)" verbatim + + + + + Extended + + + + + Ignores unescaped whitespace in regular expressions (/x) + leave "(/x)" verbatim + + + + + Keep selection in view + + + + + Scroll to keep the current selection in view after filtering + + + + + FindDialog + + + Find + + + + + Find what: + + + + + + Search term + + + + + + Find next occurence from current file position. + + + + + &Find Next + + + + + + + Close + + + + + QObject + + + Filter + + + + + One of the following plugins must be enabled: %1. + + + + + This plugin can only be enabled if the '%1' plugin is installed and enabled. + + + + + This plugin can only be enabled for the following game(s): %1. + + + + + + + + + + + INI file is read-only + + + + + + Mod Organizer is attempting to write to "%1" which is currently set to read-only. + + + + + + Clear the read-only flag + + + + + + Allow the write once + + + + + + The file will be set to read-only again. + + + + + + Skip this file + + + + + + Error + + + + + You can reset these choices by clicking "Reset Dialog Choices" in the General tab of the Settings + + + + + Always ask + + + + + + Remember my choice + + + + + Remember my choice for %1 + + + + + Failed to save '%1', could not create a temporary file: %2 (error %3) + Failed to save '{}', could not create a temporary file: {} (error {}) + + + + + removal of "%1" failed: %2 + + + + + removal of "%1" failed + + + + + "%1" doesn't exist (remove) + + + + + Error %1 + + + + + + You have an invalid custom browser command in the settings. + + + + + + failed to create directory "%1" + + + + + + failed to copy "%1" to "%2" + + + + + %1 B + + + + + %1 KB + + + + + %1 MB + + + + + %1 GB + + + + + %1 TB + + + + + %1 B/s + + + + + %1 KB/s + + + + + %1 MB/s + + + + + %1 GB/s + + + + + %1 TB/s + + + + + QuestionBoxMemory + + + Remember selection + + + + + Remember selection only for %1 + + + + + TaskDialog + + + Dialog + + + + + icon + + + + + dummy main text + + + + + dummy content text + + + + + dummy button + + + + + dummy checkbox + + + + + Details + + + + + TextViewer + + + Log Viewer + + + + + Placeholder + + + + + Show Whitespace + + + + + Save changes? + + + + + Do you want to save changes to %1? + + + + + failed to write to %1 + + + + + file not found: %1 + + + + + Save + + + + + TutorialControl + + + Tutorial failed to start, please check "mo_interface.log" for details. + + + + + TutorialManager + + + tutorial manager not set up yet + + + + + uibase + + + h + Time remaining hours + + + + + m + Time remaining minutes + + + + + s + Time remaining seconds + + + + diff --git a/src/version.rc b/src/version.rc index a171ba51..ae1d0eeb 100644 --- a/src/version.rc +++ b/src/version.rc @@ -3,14 +3,14 @@ // If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number. // Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser // Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha -#define VER_FILEVERSION 2,5.2 -#define VER_FILEVERSION_STR "2.5.2\0" +#define VER_FILEVERSION 2,6,0 +#define VER_FILEVERSION_STR "2.6.0-dev.0\0" VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION PRODUCTVERSION VER_FILEVERSION FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -FILEFLAGS (0) +FILEFLAGS VS_FF_PRERELEASE FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE (0) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 022cd8ca..54f02ead 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,11 @@ cmake_minimum_required(VERSION 3.16) add_executable(uibase-tests EXCLUDE_FROM_ALL) -mo2_configure_tests(uibase-tests - WARNINGS OFF DEPENDS uibase) +target_sources(uibase-tests + PRIVATE + test_formatters.cpp + test_ifiletree.cpp + test_strings.cpp +) +mo2_configure_tests(uibase-tests NO_SOURCES WARNINGS 4) +target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/tests/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt new file mode 100644 index 00000000..2a8e34a7 --- /dev/null +++ b/tests/cmake/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +project(uibase-tests-cmake) + +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(plugin SHARED) +target_sources(plugin PRIVATE plugin.cpp) +target_link_libraries(plugin PRIVATE mo2::uibase) diff --git a/tests/cmake/plugin.cpp b/tests/cmake/plugin.cpp new file mode 100644 index 00000000..8356dcd6 --- /dev/null +++ b/tests/cmake/plugin.cpp @@ -0,0 +1,4 @@ +#include + +class Plugin : public MOBase::IPlugin +{}; diff --git a/tests/test_formatters.cpp b/tests/test_formatters.cpp index 94fc2640..65b5d079 100644 --- a/tests/test_formatters.cpp +++ b/tests/test_formatters.cpp @@ -6,7 +6,7 @@ #include #include -#include "formatters.h" +#include #include diff --git a/tests/test_ifiletree.cpp b/tests/test_ifiletree.cpp index aefacd34..3a903ad6 100644 --- a/tests/test_ifiletree.cpp +++ b/tests/test_ifiletree.cpp @@ -7,7 +7,7 @@ #include #include -#include "ifiletree.h" +#include std::ostream& operator<<(std::ostream& os, const QString& str) { diff --git a/tests/test_strings.cpp b/tests/test_strings.cpp new file mode 100644 index 00000000..69dadbbf --- /dev/null +++ b/tests/test_strings.cpp @@ -0,0 +1,33 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include + +#include + +using namespace MOBase; + +TEST(StringsTest, IEquals) +{ + ASSERT_TRUE(iequals("hello world", "HelLO WOrlD")); +} + +TEST(StringsTest, IReplaceAll) +{ + auto ireplace_all = [](std::string_view input, std::string_view search, + std::string_view replace) { + std::string s_input{input}; + MOBase::ireplace_all(s_input, search, replace); + return s_input; + }; + + ASSERT_EQ("", ireplace_all("", "world", "MO2")); + ASSERT_EQ("Hello World!", ireplace_all("Hello World!", "Test", "MO2")); + ASSERT_EQ("replace a stuff with a stuff a", + ireplace_all("replace some stuff with some stuff some", "some", "a")); + ASSERT_EQ("replace a stuff with a stuff som", + ireplace_all("replace some stuff with some stuff som", "some", "a")); + ASSERT_EQ("1YYY3YYY2", ireplace_all("1aBc3AbC2", "abC", "YYY")); +} diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 00000000..40e0b2a4 --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,21 @@ +{ + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "09c389ef37bcec89b28f13105be959fecf4f3975", + "packages": ["mo2-cmake", "spdlog"] + } + ] +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 00000000..563f4f91 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,15 @@ +{ + "dependencies": ["mo2-cmake", "spdlog"], + "overrides": [ + { + "name": "spdlog", + "version": "1.14.1" + } + ], + "features": { + "testing": { + "description": "Build UI Base tests.", + "dependencies": ["gtest"] + } + } +} From 5f7658ec758c4e66cb1531331728a7b1243918cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Mon, 5 Aug 2024 11:40:46 +0200 Subject: [PATCH 02/28] Move to custom semver versioning for MO2. (#154) --- .gitignore | 1 + include/uibase/exceptions.h | 1 + include/uibase/imoinfo.h | 8 +- include/uibase/versioning.h | 181 +++++++++++++++++++++++ src/CMakeLists.txt | 4 +- src/iplugininstaller.cpp | 18 --- src/pluginsetting.cpp | 24 ---- src/versioning.cpp | 278 ++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/cmake/CMakeLists.txt | 1 + tests/test_versioning.cpp | 90 ++++++++++++ 11 files changed, 562 insertions(+), 45 deletions(-) create mode 100644 include/uibase/versioning.h delete mode 100644 src/iplugininstaller.cpp delete mode 100644 src/pluginsetting.cpp create mode 100644 src/versioning.cpp create mode 100644 tests/test_versioning.cpp diff --git a/.gitignore b/.gitignore index bcdbd6a5..a55b4baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ edit +.vscode CMakeLists.txt.user /msbuild.log /*std*.log diff --git a/include/uibase/exceptions.h b/include/uibase/exceptions.h index 433b9be7..c89a4833 100644 --- a/include/uibase/exceptions.h +++ b/include/uibase/exceptions.h @@ -3,6 +3,7 @@ #include +#include #include #include "dllimport.h" diff --git a/include/uibase/imoinfo.h b/include/uibase/imoinfo.h index e4f072a8..bf6588d5 100644 --- a/include/uibase/imoinfo.h +++ b/include/uibase/imoinfo.h @@ -35,6 +35,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "imodlist.h" #include "iprofile.h" #include "versioninfo.h" +#include "versioning.h" namespace MOBase { @@ -121,7 +122,12 @@ class QDLLEXPORT IOrganizer : public QObject /** * @return the running version of Mod Organizer */ - virtual VersionInfo appVersion() const = 0; + [[deprecated]] virtual VersionInfo appVersion() const = 0; + + /** + * @return the running version of Mod Organizer + */ + virtual Version version() const = 0; /** * @brief create a new mod with the specified name diff --git a/include/uibase/versioning.h b/include/uibase/versioning.h new file mode 100644 index 00000000..c4f95419 --- /dev/null +++ b/include/uibase/versioning.h @@ -0,0 +1,181 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "dllimport.h" +#include "exceptions.h" + +namespace MOBase +{ + +class InvalidVersionException : public Exception +{ +public: + using Exception::Exception; +}; + +// class representing a Version object +// +// valid versions are an "extension" of SemVer (see https://semver.org/) with the +// following tweaks: +// - version can have a sub-patch, i.e., x.y.z.p, which are normally not allowed by +// SemVer +// - non-integer pre-release identifiers are limited to dev, alpha (a), beta (b) and rc, +// and dev is lower than alpha (according to SemVer, the pre-release should be +// ordered alphabetically) +// - the '-' between version and pre-release can be made optional, and also the '.' +// between pre-releases segment +// +// the extension from SemVer are only meant to be used by MO2 and USVFS versioning, +// plugins and extensions should follow SemVer standard (and not use dev), this is +// mainly +// - for back-compatibility purposes, because USVFS versioning contains sub-patches and +// there are old MO2 releases with sub-patch +// - because MO2 is not going to become MO3, so having an extra level make sense +// +// unlike VersionInfo, this class is immutable and only hold valid versions +// +class QDLLEXPORT Version +{ +public: + enum class ParseMode + { + // official semver parsing with pre-release limited to dev, alpha/a, beta/b and rc + // + SemVer, + + // MO2 parsing, e.g., 2.5.1rc1 - this either parse a string with no pre-release + // information (e.g. 2.5.1) or with a single pre-release + a version (e.g., 2.5.1a1 + // or 2.5.2rc1) + // + // this mode can parse sub-patch (SemVer mode cannot) + // + MO2 + }; + + enum class FormatMode + { + // show subpatch even if subpatch is 0 + // + ForceSubPatch = 0b0001, + + // do not add separators between version and pre-release (-) or between pre-release + // segments (.) + // + NoSeparator = 0b0010, + + // uses short form for alpha and beta (a/b instead of alpha/beta) + // + ShortAlphaBeta = 0b0100, + + // do not add metadata even if present + // + NoMetadata = 0b1000 + }; + Q_DECLARE_FLAGS(FormatModes, FormatMode); + + // condensed format, no separator, short alpha/beta and no metadata + // + static constexpr auto FormatCondensed = FormatModes{ + FormatMode::NoSeparator, FormatMode::ShortAlphaBeta, FormatMode::NoMetadata}; + + enum class ReleaseType + { + Development, // -dev + Alpha, // -alpha, -a + Beta, // -beta, -b + ReleaseCandidate, // -rc + }; + using enum ReleaseType; + +public: // parsing + // parse version from the given string, throw InvalidVersionException if the string + // cannot be parsed + // + static Version parse(QString const& value, ParseMode mode = ParseMode::SemVer); + +public: // constructors + Version(int major, int minor, int patch, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, QString metadata = {}); + + Version(int major, int minor, int patch, ReleaseType type, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata = {}); + + Version(int major, int minor, int patch, ReleaseType type, int prerelease, + QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata = {}); + + Version(int major, int minor, int patch, int subpatch, + std::vector> prereleases, + QString metadata = {}); + +public: // special member functions + Version(const Version&) = default; + Version(Version&&) = default; + + Version& operator=(const Version&) = default; + Version& operator=(Version&&) = default; + +public: + // check if this version corresponds to a pre-release version (dev, alpha, beta, etc.) + // + bool isPreRelease() const { return !m_PreReleases.empty(); } + + // retrieve major, minor, patch and sub-patch of this version + // + int major() const { return m_Major; } + int minor() const { return m_Minor; } + int patch() const { return m_Patch; } + int subpatch() const { return m_SubPatch; } + + // retrieve pre-releases information for this version + // + const auto& preReleases() const { return m_PreReleases; } + + // retrieve build metadata, if any, otherwise return an empty string + // + const auto& buildMetadata() const { return m_BuildMetadata; } + + // convert this version to a string + // + QString string(const FormatModes& modes = {}) const; + +private: + // major.minor.patch + int m_Major, m_Minor, m_Patch, m_SubPatch; + + // pre-release information + std::vector> m_PreReleases; + + // metadata + QString m_BuildMetadata; +}; + +QDLLEXPORT std::strong_ordering operator<=>(const Version& lhs, const Version& rhs); + +inline bool operator==(const Version& lhs, const Version& rhs) +{ + return (lhs <=> rhs) == 0; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Version::FormatModes); + +} // namespace MOBase + +template +struct std::formatter : std::formatter +{ + template + FmtContext::iterator format(const MOBase::Version& v, FmtContext& ctx) const + { + return std::formatter::format(v.string(), ctx); + } +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b096a67..fa1d0a66 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,6 +30,7 @@ set(root_headers ../include/uibase/steamutility.h ../include/uibase/strings.h ../include/uibase/utility.h + ../include/uibase/versioning.h ../include/uibase/versioninfo.h ) set(interface_headers @@ -115,7 +116,6 @@ mo2_target_sources(uibase nxmurl.cpp pch.cpp pluginrequirements.cpp - pluginsetting.cpp registry.cpp report.cpp safewritefile.cpp @@ -123,6 +123,7 @@ mo2_target_sources(uibase steamutility.cpp strings.cpp utility.cpp + versioning.cpp versioninfo.cpp ) @@ -133,7 +134,6 @@ mo2_target_sources(uibase ifiletree.cpp imodrepositorybridge.cpp imoinfo.cpp - iplugininstaller.cpp ) mo2_target_sources(uibase diff --git a/src/iplugininstaller.cpp b/src/iplugininstaller.cpp deleted file mode 100644 index 79e62472..00000000 --- a/src/iplugininstaller.cpp +++ /dev/null @@ -1,18 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer 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. - -Mod Organizer 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 Mod Organizer. If not, see . -*/ diff --git a/src/pluginsetting.cpp b/src/pluginsetting.cpp deleted file mode 100644 index cce84eb6..00000000 --- a/src/pluginsetting.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/* -Copyright (C) 2012 Sebastian Herbord. All rights reserved. - -This file is part of Mod Organizer. - -Mod Organizer 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. - -Mod Organizer 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 Mod Organizer. If not, see . -*/ - -#include "pluginsetting.h" - -namespace MOBase -{ -} // namespace MOBase diff --git a/src/versioning.cpp b/src/versioning.cpp new file mode 100644 index 00000000..ddc0ed6a --- /dev/null +++ b/src/versioning.cpp @@ -0,0 +1,278 @@ +#include "versioning.h" + +#include +#include +#include + +#include "formatters.h" + +// official semver regex +static const QRegularExpression s_SemVerStrictRegEx{ + R"(^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX +static const QRegularExpression s_SemVerMO2RegEx{ + R"(^v?(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:\.(?P0|[1-9]\d*))?(?:(?Pdev|a|alpha|b|beta|rc)(?P0|[1-9](?:[.0-9])*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// match from value to release type +static const std::unordered_map + s_StringToRelease{{"dev", MOBase::Version::Development}, + {"alpha", MOBase::Version::Alpha}, + {"a", MOBase::Version::Alpha}, + {"beta", MOBase::Version::Beta}, + {"b", MOBase::Version::Beta}, + {"rc", MOBase::Version::ReleaseCandidate}}; + +namespace MOBase +{ + +namespace +{ + + Version parseVersionSemVer(QString const& value) + { + const auto match = s_SemVerStrictRegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + std::vector> prereleases; + for (auto& part : match.captured("prerelease") + .split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) { + // try to extract an int + bool ok = true; + const auto intValue = part.toInt(&ok); + if (ok) { + prereleases.push_back(intValue); + continue; + } + + // check if we have a valid prerelease type + const auto it = s_StringToRelease.find(part.toLower()); + if (it == s_StringToRelease.end()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid prerelease type: '{}'", part))); + } + + prereleases.push_back(it->second); + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, 0, prereleases, buildMetadata); + } + + Version parseVersionMO2(QString const& value) + { + const auto match = s_SemVerMO2RegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + const auto subpatch = match.captured("subpatch").toInt(); + + // unlike semver, the regex will only match valid values + std::vector> prereleases; + if (match.hasCaptured("type")) { + prereleases.push_back(s_StringToRelease.at(match.captured("type"))); + + // for version with decimal point, e.g., 2.4.1rc1.1, we split the components into + // pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc, + // 1, 1} + // + for (const auto& preVersion : + match.captured("prerelease").split(".", Qt::SkipEmptyParts)) { + prereleases.push_back(preVersion.toInt()); + } + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, subpatch, prereleases, buildMetadata); + } + +} // namespace + +Version Version::parse(QString const& value, ParseMode mode) +{ + return mode == ParseMode::SemVer ? parseVersionSemVer(value) : parseVersionMO2(value); +} + +// constructors + +Version::Version(int major, int minor, int patch, QString metadata) + : Version(major, minor, patch, 0, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, QString metadata) + : Version(major, minor, patch, 0, type, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{type}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, int prerelease, + QString metadata) + : Version(major, minor, patch, 0, type, prerelease, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata) + : Version(major, minor, patch, subpatch, {type, prerelease}, std::move(metadata)) +{} + +Version::Version(int major, int minor, int patch, int subpatch, + std::vector> prereleases, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{std::move(prereleases)}, m_BuildMetadata{std::move(metadata)} +{} + +// string + +QString Version::string(const FormatModes& modes) const +{ + const bool noSeparator = modes.testFlag(FormatMode::NoSeparator); + const bool shortAlphaBeta = modes.testFlag(FormatMode::ShortAlphaBeta); + auto value = std::format("{}.{}.{}", m_Major, m_Minor, m_Patch); + + if (m_SubPatch || modes.testFlag(FormatMode::ForceSubPatch)) { + value += std::format(".{}", m_SubPatch); + } + + if (!m_PreReleases.empty()) { + if (!noSeparator) { + value += "-"; + } + for (std::size_t i = 0; i < m_PreReleases.size(); ++i) { + value += std::visit( + [shortAlphaBeta](auto const& pre) -> std::string { + if constexpr (std::is_same_v) { + switch (pre) { + case Development: + return "dev"; + case Alpha: + return shortAlphaBeta ? "a" : "alpha"; + case Beta: + return shortAlphaBeta ? "b" : "beta"; + case ReleaseCandidate: + return "rc"; + } + return ""; + } else { + return std::to_string(pre); + } + }, + m_PreReleases[i]); + if (!noSeparator && i < m_PreReleases.size() - 1) { + value += "."; + } + } + } + + if (!modes.testFlag(FormatMode::NoMetadata) && !m_BuildMetadata.isEmpty()) { + value += "+" + m_BuildMetadata.toStdString(); + } + + return QString::fromStdString(value); +} + +namespace +{ + // consume the given iterator until the given end iterator or until a non-zero value + // is found + // + template + It consumePreReleaseZeros(It it, It end) + { + for (; it != end; ++it) { + if (!std::holds_alternative(*it) != 0 || std::get(*it) != 0) { + break; + } + } + return it; + }; +} // namespace + +std::strong_ordering operator<=>(const Version& lhs, const Version& rhs) +{ + auto mmp_cmp = + std::forward_as_tuple(lhs.major(), lhs.minor(), lhs.patch(), lhs.subpatch()) <=> + std::forward_as_tuple(rhs.major(), rhs.minor(), rhs.patch(), rhs.subpatch()); + + // major.minor.patch have precedence over everything else + if (mmp_cmp != std::strong_ordering::equal) { + return mmp_cmp; + } + + // handle cases were one is a pre-release and not the other - the pre-release is + // "less" than the release + if (lhs.isPreRelease() && !rhs.isPreRelease()) { + return std::strong_ordering::less; + } + + if (!lhs.isPreRelease() && rhs.isPreRelease()) { + return std::strong_ordering::greater; + } + + // compare pre-release fields + auto lhsIt = lhs.preReleases().begin(), rhsIt = rhs.preReleases().begin(); + for (; lhsIt != lhs.preReleases().end() && rhsIt != rhs.preReleases().end(); + ++lhsIt, ++rhsIt) { + + const auto &lhsPre = *lhsIt, rhsPre = *rhsIt; + + // if one is alpha/beta/etc. and the other is numeric, the alpha/beta/etc. is lower + // than the numeric one, which matches the index + auto pre_cmp = lhsPre.index() <=> rhsPre.index(); + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + + // compare the actual values + pre_cmp = lhsPre <=> rhsPre; + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + } + + // the code below does not follow semver 100% (I think) - basically, this makes stuff + // like 2.4.1rc1.0 equals to 2.4.1rc1, which according to semver is probably not right + // but is probably best for us + // + + // if we land here, we have consumed one of the pre-release, we skip all the 0 in the + // remaining one + lhsIt = consumePreReleaseZeros(lhsIt, lhs.preReleases().end()); + rhsIt = consumePreReleaseZeros(rhsIt, rhs.preReleases().end()); + + const auto lhsConsumed = lhsIt == lhs.preReleases().end(), + rhsConsumed = rhsIt == rhs.preReleases().end(); + + if (lhsConsumed && rhsConsumed) { + return std::strong_ordering::equal; + } else if (!lhsConsumed) { + return std::strong_ordering::greater; + } else { + return std::strong_ordering::less; + } +} + +} // namespace MOBase diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 54f02ead..ac17234a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ target_sources(uibase-tests test_formatters.cpp test_ifiletree.cpp test_strings.cpp + test_versioning.cpp ) mo2_configure_tests(uibase-tests NO_SOURCES WARNINGS 4) target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/tests/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt index 2a8e34a7..e1d636f7 100644 --- a/tests/cmake/CMakeLists.txt +++ b/tests/cmake/CMakeLists.txt @@ -7,3 +7,4 @@ find_package(mo2-uibase CONFIG REQUIRED) add_library(plugin SHARED) target_sources(plugin PRIVATE plugin.cpp) target_link_libraries(plugin PRIVATE mo2::uibase) +set_target_properties(plugin PROPERTIES CXX_STANDARD 20) diff --git a/tests/test_versioning.cpp b/tests/test_versioning.cpp new file mode 100644 index 00000000..08a346fd --- /dev/null +++ b/tests/test_versioning.cpp @@ -0,0 +1,90 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include +#include + +#include + +#include + +using namespace MOBase; + +using enum Version::ReleaseType; +using ParseMode = Version::ParseMode; + +TEST(VersioningTest, VersionParse) +{ + // TODO: add exceptions test + + // semver + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0")); + ASSERT_EQ(Version(1, 0, 0, Development, 1), Version::parse("1.0.0-dev.1")); + ASSERT_EQ(Version(1, 0, 0, Development, 2), Version::parse("1.0.0-dev.2")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-a")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-alpha")); + ASSERT_EQ(Version(1, 0, 0, 0, {Alpha, 1, Beta}), Version::parse("1.0.0-alpha.1.b")); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0-beta.2")); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), Version::parse("2.5.2-rc.1")); + + // mo2 + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 1), + Version::parse("1.0.0dev1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 2), + Version::parse("1.0.0dev2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0a1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0alpha1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 4, 1, 0, {ReleaseCandidate, 1, 1}), + Version::parse("2.4.1rc1.1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 2, 2, 1, Beta, 2), + Version::parse("2.2.2.1beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), + Version::parse("v2.5.2rc1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 2), + Version::parse("2.5.2rc2", ParseMode::MO2)); +} + +TEST(VersioningTest, VersionString) +{ + ASSERT_EQ("1.0.0", Version(1, 0, 0).string()); + ASSERT_EQ("1.0.0-dev.1", Version(1, 0, 0, Development, 1).string()); + ASSERT_EQ("1.0.0-dev.2", Version(1, 0, 0, Development, 2).string()); + ASSERT_EQ("1.0.0-alpha", Version(1, 0, 0, Alpha).string()); + ASSERT_EQ("1.0.0-alpha.1.beta", Version(1, 0, 0, 0, {Alpha, 1, Beta}).string()); + ASSERT_EQ("1.0.0-beta.2", Version(1, 0, 0, Beta, 2).string()); + ASSERT_EQ("2.5.2-rc.1", Version(2, 5, 2, ReleaseCandidate, 1).string()); + ASSERT_EQ("2.5.2rc1", + Version(2, 5, 2, ReleaseCandidate, 1).string(Version::FormatCondensed)); +} + +TEST(VersioningTest, VersionCompare) +{ + // shortcut + using v = Version; + + // test from https://semver.org/ + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0)); + ASSERT_TRUE(v(2, 0, 0) < v(2, 1, 0)); + ASSERT_TRUE(v(2, 1, 0) < v(2, 1, 1)); + + ASSERT_TRUE(v(1, 0, 0, Alpha) < v(1, 0, 0, Alpha, 1)); + ASSERT_TRUE(v(1, 0, 0, Alpha, 1) < v(1, 0, 0, 0, {Alpha, Beta})); + ASSERT_TRUE(v(1, 0, 0, 0, {Alpha, Beta}) < v(1, 0, 0, 1)); + ASSERT_TRUE(v(1, 0, 0, Beta) < v(1, 0, 0, Beta, 2)); + ASSERT_TRUE(v(1, 0, 0, Beta, 2) < v(1, 0, 0, Beta, 11)); + ASSERT_TRUE(v(1, 0, 0, Beta, 11) < v(1, 0, 0, ReleaseCandidate, 1)); + ASSERT_TRUE(v(1, 0, 0, ReleaseCandidate, 0) < v(1, 0, 0)); + + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) == + v(2, 4, 1, ReleaseCandidate, 1)); + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(2, 4, 1, ReleaseCandidate, 1) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0, Alpha)); +} From d7faaf801220a8c4138d78aa2ba5a380f31c175f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Mon, 5 Aug 2024 13:43:27 +0200 Subject: [PATCH 03/28] Update Qt version in CI and bump MO2 VCPKG registry baseline. --- .github/workflows/build.yml | 2 +- vcpkg-configuration.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 893fa507..f685a376 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v3 with: - version: 6.7.0 + version: 6.7.1 modules: cache: true diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 40e0b2a4..2e5c2a90 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -14,7 +14,7 @@ { "kind": "git", "repository": "https://github.com/ModOrganizer2/vcpkg-registry", - "baseline": "09c389ef37bcec89b28f13105be959fecf4f3975", + "baseline": "6e9492a38a7d5bfeeeaf80723fa821b42f3a26a8", "packages": ["mo2-cmake", "spdlog"] } ] From 10f6f71278f9d34665a769d87e581fbd97705fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Fri, 9 Aug 2024 12:47:13 +0200 Subject: [PATCH 04/28] Allow for external mo2-cmake (#160) * Create standalone feature for mo2-cmake. * Setup a QCoreApplication in tests main. --- .github/workflows/build.yml | 2 +- CMakePresets.json | 15 +++++++++------ tests/CMakeLists.txt | 3 ++- tests/test_main.cpp | 15 +++++++++++++++ tests/test_strings.cpp | 9 +++++++++ tests/translations/tests_en.qm | Bin 0 -> 141 bytes tests/translations/tests_en.ts | 11 +++++++++++ tests/translations/tests_fr.qm | Bin 0 -> 147 bytes tests/translations/tests_fr.ts | 11 +++++++++++ vcpkg-configuration.json | 21 --------------------- vcpkg.json | 21 ++++++++++++++++++++- 11 files changed, 78 insertions(+), 30 deletions(-) create mode 100644 tests/test_main.cpp create mode 100644 tests/translations/tests_en.qm create mode 100644 tests/translations/tests_en.ts create mode 100644 tests/translations/tests_fr.qm create mode 100644 tests/translations/tests_fr.ts delete mode 100644 vcpkg-configuration.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f685a376..e5ceb1be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: - name: Configure UI Base build shell: pwsh run: | - cmake --preset vs2022-windows ` + cmake --preset vs2022-windows-standalone ` "-DCMAKE_PREFIX_PATH=${env:QT_ROOT_DIR}\msvc2019_64" ` "-DCMAKE_INSTALL_PREFIX=install" -DUIBASE_TESTING=ON diff --git a/CMakePresets.json b/CMakePresets.json index 0c06d1a3..f0e338e3 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -50,13 +50,16 @@ "inherits": ["cmake-dev", "vcpkg-dev"], "name": "vs2022-windows", "toolset": "v143" - } - ], - "buildPresets": [ + }, { - "name": "vs2022-windows", - "resolvePackageReferences": "on", - "configurePreset": "vs2022-windows" + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone;testing" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" } ], "version": 4 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ac17234a..7ff9e192 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,10 +3,11 @@ cmake_minimum_required(VERSION 3.16) add_executable(uibase-tests EXCLUDE_FROM_ALL) target_sources(uibase-tests PRIVATE + test_main.cpp test_formatters.cpp test_ifiletree.cpp test_strings.cpp test_versioning.cpp ) -mo2_configure_tests(uibase-tests NO_SOURCES WARNINGS 4) +mo2_configure_tests(uibase-tests NO_SOURCES NO_MAIN NO_MOCK WARNINGS 4) target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/tests/test_main.cpp b/tests/test_main.cpp new file mode 100644 index 00000000..0399b433 --- /dev/null +++ b/tests/test_main.cpp @@ -0,0 +1,15 @@ +#include + +#include +#include + +int main(int argc, char** argv) +{ + QCoreApplication app(argc, argv); + QTranslator translator; + if (translator.load("tests_fr", "tests/translations")) { + app.installTranslator(&translator); + } + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/test_strings.cpp b/tests/test_strings.cpp index 69dadbbf..56e75c9b 100644 --- a/tests/test_strings.cpp +++ b/tests/test_strings.cpp @@ -3,6 +3,8 @@ #include #pragma warning(pop) +#include + #include #include @@ -31,3 +33,10 @@ TEST(StringsTest, IReplaceAll) ireplace_all("replace some stuff with some stuff som", "some", "a")); ASSERT_EQ("1YYY3YYY2", ireplace_all("1aBc3AbC2", "abC", "YYY")); } + +// this is more a tests of the tests +TEST(StringsTest, Translation) +{ + ASSERT_EQ("Traduction en Français", + QCoreApplication::translate("uibase-tests", "Translate to French")); +} diff --git a/tests/translations/tests_en.qm b/tests/translations/tests_en.qm new file mode 100644 index 0000000000000000000000000000000000000000..d5ca50195d8899565e374e2405b25297d85c8ced GIT binary patch literal 141 zcmcE7ks@*G{hX<16=n7(EZlo{IRgU&YieG6Xs{EI#lil4=?@^ykO{<5%s@hoA%vlb zA(0`Ep_m~DNS83AGAIDad?4utlt~53Br{}ifHbiIv2aLHVqS4hVo9n(Nxp(xQEFat c20Kukr!+Guu{c$?B(=Dtn6U#W!o + + + + uibase-tests + + Translate to French + Translate to French + + + diff --git a/tests/translations/tests_fr.qm b/tests/translations/tests_fr.qm new file mode 100644 index 0000000000000000000000000000000000000000..80958d6e11a850ede8d6266c2a39254a0abce8ba GIT binary patch literal 147 zcmcE7ks@*G{hX<16=n7(EZlo{IRgU&Yg$pfTaXiw#lil4=?@^ykO{;I%s@hiA%vlb zA(0`4p_Cz+p#&(A&ydHUz>o?i-N15r49|foG8u|FKw8;=SU998F|Rl$u_RTYBwxX; gC^auRgB>W&Q<|BSSe&X`l3H9+%-8`GVPa+k0Fgu_wEzGB literal 0 HcmV?d00001 diff --git a/tests/translations/tests_fr.ts b/tests/translations/tests_fr.ts new file mode 100644 index 00000000..eed02a06 --- /dev/null +++ b/tests/translations/tests_fr.ts @@ -0,0 +1,11 @@ + + + + + uibase-tests + + Translate to French + Traduction en Français + + + diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json deleted file mode 100644 index 2e5c2a90..00000000 --- a/vcpkg-configuration.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "default-registry": { - "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", - "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7" - }, - "registries": [ - { - "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", - "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7", - "packages": ["boost*", "boost-*"] - }, - { - "kind": "git", - "repository": "https://github.com/ModOrganizer2/vcpkg-registry", - "baseline": "6e9492a38a7d5bfeeeaf80723fa821b42f3a26a8", - "packages": ["mo2-cmake", "spdlog"] - } - ] -} diff --git a/vcpkg.json b/vcpkg.json index 563f4f91..6c1e8c9d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,5 +1,5 @@ { - "dependencies": ["mo2-cmake", "spdlog"], + "dependencies": ["spdlog"], "overrides": [ { "name": "spdlog", @@ -7,9 +7,28 @@ } ], "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + }, "testing": { "description": "Build UI Base tests.", "dependencies": ["gtest"] } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "8ae59b5b1329a51875abc71d528da93d9c3e8972" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "84ff92223433d101738a3c6cef96fa6ae6a6f302", + "packages": ["mo2-cmake", "spdlog"] + } + ] } } From 265b1891cf9065f8e1ed4bd3b26347be7de4ecb9 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 05/28] Start working on extensions. --- include/uibase/extension.h | 173 +++++++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 2 + src/extension.cpp | 73 ++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 include/uibase/extension.h create mode 100644 src/extension.cpp diff --git a/include/uibase/extension.h b/include/uibase/extension.h new file mode 100644 index 00000000..b70ea749 --- /dev/null +++ b/include/uibase/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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fa1d0a66..ed25e1d0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ set(root_headers ../include/uibase/eventfilter.h ../include/uibase/exceptions.h ../include/uibase/executableinfo.h + ../include/uibase/extension.h ../include/uibase/filemapping.h ../include/uibase/filesystemutilities.h ../include/uibase/guessedvalue.h @@ -108,6 +109,7 @@ mo2_target_sources(uibase errorcodes.cpp eventfilter.cpp executableinfo.cpp + extension.cpp filesystemutilities.cpp guessedvalue.cpp json.cpp 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 From d4f9d149da592313bfed978782c2bb0113eb492c 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 06/28] Add theme and translation for extensions. --- include/uibase/extension.h | 55 +++++++++++++----------------- include/uibase/theme.h | 65 ++++++++++++++++++++++++++++++++++++ include/uibase/translation.h | 62 ++++++++++++++++++++++++++++++++++ include/uibase/utility.h | 2 ++ src/CMakeLists.txt | 15 ++++++++- src/extension.cpp | 47 +------------------------- src/theme.cpp | 21 ++++++++++++ src/uibase_en.ts | 26 +++++++-------- src/utility.cpp | 10 ++++++ 9 files changed, 212 insertions(+), 91 deletions(-) create mode 100644 include/uibase/theme.h create mode 100644 include/uibase/translation.h create mode 100644 src/theme.cpp diff --git a/include/uibase/extension.h b/include/uibase/extension.h index b70ea749..bac8dadb 100644 --- a/include/uibase/extension.h +++ b/include/uibase/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/include/uibase/theme.h b/include/uibase/theme.h new file mode 100644 index 00000000..624f1a01 --- /dev/null +++ b/include/uibase/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/include/uibase/translation.h b/include/uibase/translation.h new file mode 100644 index 00000000..b1dd66f9 --- /dev/null +++ b/include/uibase/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/include/uibase/utility.h b/include/uibase/utility.h index 30ade0c7..818c7860 100644 --- a/include/uibase/utility.h +++ b/include/uibase/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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ed25e1d0..cd87d084 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,7 +11,6 @@ set(root_headers ../include/uibase/eventfilter.h ../include/uibase/exceptions.h ../include/uibase/executableinfo.h - ../include/uibase/extension.h ../include/uibase/filemapping.h ../include/uibase/filesystemutilities.h ../include/uibase/guessedvalue.h @@ -34,6 +33,11 @@ set(root_headers ../include/uibase/versioning.h ../include/uibase/versioninfo.h ) +set(extension_headers + ../include/uibase/extension.h + ../include/uibase/theme.h + ../include/uibase/translation.h +) set(interface_headers ../include/uibase/ifiletree.h ../include/uibase/iinstallationmanager.h @@ -129,6 +133,14 @@ mo2_target_sources(uibase versioninfo.cpp ) +mo2_target_sources(uibase + FOLDER src/extensions + PRIVATE + ${extension_headers} + extension.cpp + theme.cpp +) + mo2_target_sources(uibase FOLDER src/interfaces PRIVATE @@ -183,6 +195,7 @@ target_sources(uibase BASE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../include FILES ${root_headers} + ${extension_headers} ${interface_headers} ${tutorial_headers} ${widget_headers} 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/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/uibase_en.ts b/src/uibase_en.ts index 7f02320f..d5dd17a5 100644 --- a/src/uibase_en.ts +++ b/src/uibase_en.ts @@ -223,52 +223,52 @@ - + %1 B - + %1 KB - + %1 MB - + %1 GB - + %1 TB - + %1 B/s - + %1 KB/s - + %1 MB/s - + %1 GB/s - + %1 TB/s @@ -386,19 +386,19 @@ uibase - + h Time remaining hours - + m Time remaining minutes - + s Time remaining seconds diff --git a/src/utility.cpp b/src/utility.cpp index cca887a0..f9a98565 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -736,12 +736,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]; From 57b4f9bc9c45a8772bc2b2c1e6677e915729d37f 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 07/28] Implement loading theme and translation extensions. --- include/uibase/extension.h | 235 ++++++++++++++++++++++++------------- src/extension.cpp | 220 ++++++++++++++++++++++++++++++++-- 2 files changed, 368 insertions(+), 87 deletions(-) diff --git a/include/uibase/extension.h b/include/uibase/extension.h index bac8dadb..8e91aa81 100644 --- a/include/uibase/extension.h +++ b/include/uibase/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 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 From 7bf2d15863cc9fe06d20a93fe2f8726a2277f5f7 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 08/28] Update for extensions. --- include/uibase/extension.h | 19 +++++++- include/uibase/ipluginloader.h | 65 ++++++++++++++++++++++++++ include/uibase/ipluginproxy.h | 83 ---------------------------------- src/CMakeLists.txt | 2 +- src/extension.cpp | 35 +++++++++++++- 5 files changed, 116 insertions(+), 88 deletions(-) create mode 100644 include/uibase/ipluginloader.h delete mode 100644 include/uibase/ipluginproxy.h diff --git a/include/uibase/extension.h b/include/uibase/extension.h index 8e91aa81..acf10beb 100644 --- a/include/uibase/extension.h +++ b/include/uibase/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/include/uibase/ipluginloader.h b/include/uibase/ipluginloader.h new file mode 100644 index 00000000..f18f886a --- /dev/null +++ b/include/uibase/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/include/uibase/ipluginproxy.h b/include/uibase/ipluginproxy.h deleted file mode 100644 index b0b8d843..00000000 --- a/include/uibase/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/CMakeLists.txt b/src/CMakeLists.txt index cd87d084..b697a6cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ set(root_headers ) set(extension_headers ../include/uibase/extension.h + ../include/uibase/ipluginloader.h ../include/uibase/theme.h ../include/uibase/translation.h ) @@ -56,7 +57,6 @@ set(interface_headers ../include/uibase/ipluginlist.h ../include/uibase/ipluginmodpage.h ../include/uibase/ipluginpreview.h - ../include/uibase/ipluginproxy.h ../include/uibase/iplugintool.h ../include/uibase/iprofile.h ../include/uibase/isavegame.h 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 From d1204972f31f4334473991da6c9b43eadf535380 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 09/28] 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 2a40eb7e950a3b56718508e792ebc9a9a0bc7868 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 10/28] Allow specifying plugins in extensions. --- include/uibase/extension.h | 4 ++-- src/extension.cpp | 32 ++++++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/include/uibase/extension.h b/include/uibase/extension.h index acf10beb..65557155 100644 --- a/include/uibase/extension.h +++ b/include/uibase/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; 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) From ee70f02669dfe542ef2a8c9b6d6f0dff5ae5a2d3 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 11/28] Add extension list interface. Start working on requirements. --- include/uibase/extension.h | 34 ++++------------ include/uibase/iextensionlist.h | 51 +++++++++++++++++++++++ include/uibase/imoinfo.h | 6 +++ include/uibase/requirements.h | 72 +++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 3 ++ src/extension.cpp | 11 +++-- src/requirements.cpp | 47 +++++++++++++++++++++ src/tutorialcontrol.cpp | 13 +++--- 8 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 include/uibase/iextensionlist.h create mode 100644 include/uibase/requirements.h create mode 100644 src/requirements.cpp diff --git a/include/uibase/extension.h b/include/uibase/extension.h index 65557155..b94a8fe6 100644 --- a/include/uibase/extension.h +++ b/include/uibase/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/include/uibase/iextensionlist.h b/include/uibase/iextensionlist.h new file mode 100644 index 00000000..952ba16a --- /dev/null +++ b/include/uibase/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/include/uibase/imoinfo.h b/include/uibase/imoinfo.h index bf6588d5..df3b3383 100644 --- a/include/uibase/imoinfo.h +++ b/include/uibase/imoinfo.h @@ -41,6 +41,7 @@ namespace MOBase { class IFileTree; +class IExtensionList; class IModInterface; class IModRepositoryBridge; class IDownloadManager; @@ -313,6 +314,11 @@ class QDLLEXPORT IOrganizer : public QObject */ virtual 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/include/uibase/requirements.h b/include/uibase/requirements.h new file mode 100644 index 00000000..1d71d52d --- /dev/null +++ b/include/uibase/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/CMakeLists.txt b/src/CMakeLists.txt index b697a6cd..f96f9e29 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,7 +35,9 @@ set(root_headers ) set(extension_headers ../include/uibase/extension.h + ../include/uibase/iextensionlist.h ../include/uibase/ipluginloader.h + ../include/uibase/requirements.h ../include/uibase/theme.h ../include/uibase/translation.h ) @@ -139,6 +141,7 @@ mo2_target_sources(uibase ${extension_headers} extension.cpp theme.cpp + requirements.cpp ) mo2_target_sources(uibase 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/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/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); From 1b625416536ec745fca5620375c5acf3c2b35a71 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 12/28] 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 c2e37dcc8a9b7ede3546017f2d955cbe5fc6025b 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 13/28] 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()) { From 8d69d87a581eb42ea07042ec4ceda4f046aa1ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 22 Jul 2023 15:43:20 +0200 Subject: [PATCH 14/28] Re-organize extension metadata. --- include/uibase/extension.h | 3 +++ src/extension.cpp | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/include/uibase/extension.h b/include/uibase/extension.h index b94a8fe6..679319cc 100644 --- a/include/uibase/extension.h +++ b/include/uibase/extension.h @@ -62,6 +62,9 @@ class QDLLEXPORT ExtensionMetaData // const auto& json() const { return m_JsonData; } + // retrieve the content objects of the extension + QJsonObject content() const; + private: QString localized(QString const& value) const; diff --git a/src/extension.cpp b/src/extension.cpp index daa51ae3..62061635 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -45,7 +45,7 @@ namespace ExtensionMetaData::ExtensionMetaData(QJsonObject const& jsonData) : m_JsonData{jsonData} { // read basic fields - m_Identifier = jsonData["identifier"].toString(); + m_Identifier = jsonData["id"].toString(); m_Type = parseType(jsonData["type"].toString()); m_Name = jsonData["name"].toString(); m_Description = jsonData["description"].toString(); @@ -93,6 +93,21 @@ QString ExtensionMetaData::localized(QString const& value) const return result.isEmpty() ? value : result; } +QJsonObject ExtensionMetaData::content() const +{ + if (!m_JsonData.contains("content")) { + return {}; + } + + const auto value = m_JsonData["content"]; + if (!value.isObject()) { + log::error("invalid metadata for {}, 'content' should be an object", m_Identifier); + return {}; + } + + return value.toObject(); +} + IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata) : m_Path{std::move(path)}, m_MetaData{std::move(metadata)}, m_Requirements{ExtensionRequirementFactory::parseRequirements(m_MetaData)} @@ -166,7 +181,7 @@ std::unique_ptr ThemeExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) { std::vector> themes; - const auto& jsonThemes = metadata.json()["themes"].toObject(); + const auto& jsonThemes = metadata.content()["themes"].toObject(); for (auto it = jsonThemes.begin(); it != jsonThemes.end(); ++it) { const auto theme = parseTheme(path, it.key(), it.value().toObject()); if (theme) { @@ -213,7 +228,7 @@ TranslationExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) { std::vector> translations; - const auto& jsonTranslations = metadata.json()["translations"].toObject(); + const auto& jsonTranslations = metadata.content()["translations"].toObject(); for (auto it = jsonTranslations.begin(); it != jsonTranslations.end(); ++it) { const auto translation = parseTranslation(path, it.key(), it.value().toObject()); if (translation) { @@ -270,7 +285,7 @@ PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData met std::optional autodetect; std::map plugins; { - auto jsonPlugins = metadata.json()["plugins"].toObject(); + auto jsonPlugins = metadata.content()["plugins"].toObject(); if (jsonPlugins.contains("autodetect")) { autodetect = jsonPlugins["autodetect"].toBool(); } From bfa3fdff3f4204cd6deaf75ae999d0913428d8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 22 Jul 2023 15:43:30 +0200 Subject: [PATCH 15/28] Remove irrelevant methods from IPlugin. --- include/uibase/iplugin.h | 43 ------------------------------------ include/uibase/iplugingame.h | 7 ------ 2 files changed, 50 deletions(-) diff --git a/include/uibase/iplugin.h b/include/uibase/iplugin.h index aae89dcc..c86d774b 100644 --- a/include/uibase/iplugin.h +++ b/include/uibase/iplugin.h @@ -81,49 +81,6 @@ class IPlugin */ virtual QString localizedName() const { return name(); } - /** - * @brief Retrieve the name of the master plugin of this plugin. - * - * It is often easier to implement a functionality as multiple plugins in MO2, but - * ship the plugins together, e.g. as a Python module or using `createFunctions()`. In - * this case, having a master plugin (one of the plugin, or a separate one) tells MO2 - * that these plugins are linked and should also be displayed together in the UI. If - * MO2 ever implements automatic updates for plugins, the `master()` plugin will also - * be used for this purpose. - * - * @return the name of the master plugin of this plugin, or an empty string if this - * plugin does not have a master. - */ - virtual QString master() const { return ""; } - - /** - * @brief Retrieve the requirements for the plugins. - * - * This method is called right after init(). - * - * @return the requirements for this plugin. - */ - virtual std::vector> requirements() const - { - return {}; - } - - /** - * @return the author of this plugin. - */ - virtual QString author() const = 0; - - /** - * @return a short description of the plugin to be displayed to the user. - */ - virtual QString description() const = 0; - - /** - * @return the version of the plugin. This can be used to detect outdated versions of - * plugins. - */ - virtual VersionInfo version() const = 0; - /** * @return the list of configurable settings for this plugin (in the user interface). * The list may be empty. diff --git a/include/uibase/iplugingame.h b/include/uibase/iplugingame.h index a191e646..096084c7 100644 --- a/include/uibase/iplugingame.h +++ b/include/uibase/iplugingame.h @@ -64,13 +64,6 @@ class IPluginGame : public QObject, public IPlugin Q_DECLARE_FLAGS(ProfileSettings, ProfileSetting) public: - // Game plugin should not have requirements: - std::vector> - requirements() const final override - { - return {}; - } - // Game plugin can not be disabled bool enabledByDefault() const final override { return true; } From ede5d44382ef0b7ccec451d03dce9c7014a17000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 22 Jul 2023 18:40:31 +0200 Subject: [PATCH 16/28] Add author, contributors and icon to metadata. --- include/uibase/extension.h | 38 ++++++++++++++++++++++++++++++-- src/extension.cpp | 45 +++++++++++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/include/uibase/extension.h b/include/uibase/extension.h index 679319cc..702b6730 100644 --- a/include/uibase/extension.h +++ b/include/uibase/extension.h @@ -28,11 +28,26 @@ enum class ExtensionType GAME }; -class QDLLEXPORT ExtensionMetaData +class QDLLEXPORT ExtensionContributor { public: - ExtensionMetaData(QJsonObject const& jsonData); + ExtensionContributor(QString name); + + // retrieve the name of the contributor + // + const auto& name() const { return m_Name; } + +private: + ExtensionContributor() = default; + + friend class ExtensionMetaData; + QString m_Name; +}; + +class QDLLEXPORT ExtensionMetaData +{ +public: // check if that metadata object is valid // bool isValid() const; @@ -45,6 +60,14 @@ class QDLLEXPORT ExtensionMetaData // auto name() const { return localized(m_Name); } + // retrieve the author of the extension if set + // + const auto& author() const { return m_Author; } + + // retrieve the list of contributors of the extension + // + const auto& contributors() const { return m_Contributors; } + // retrieve the type of the extension // auto type() const { return m_Type; } @@ -53,6 +76,10 @@ class QDLLEXPORT ExtensionMetaData // auto description() const { return localized(m_Description); } + // retrieve the icon for the extension (might be an empty icon) + // + const auto& icon() const { return m_Icon; } + // retrieve the version of the extension. // const auto& version() const { return m_Version; } @@ -69,19 +96,26 @@ class QDLLEXPORT ExtensionMetaData QString localized(QString const& value) const; private: + friend class ExtensionFactory; + constexpr static const char* DEFAULT_TRANSLATIONS_FOLDER = "translations"; constexpr static const char* DEFAULT_STYLESHEET_PATH = "stylesheets"; ExtensionType parseType(QString const& value) const; + ExtensionMetaData(std::filesystem::path const& path, const QJsonObject& jsonData); + private: QJsonObject m_JsonData; QString m_TranslationContext; QString m_Identifier; QString m_Name; + ExtensionContributor m_Author; + std::vector m_Contributors; ExtensionType m_Type; QString m_Description; + QIcon m_Icon; VersionInfo m_Version; std::filesystem::path m_TranslationFilesPrefix; diff --git a/src/extension.cpp b/src/extension.cpp index 62061635..ffdb0260 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "log.h" @@ -40,20 +41,53 @@ namespace return files; } + // parse an author from a JSON value + // + ExtensionContributor parseContributor(QJsonValue const& value) + { + if (value.isNull()) { + return ExtensionContributor(""); + } + + // TODO: handle more fields in the future, handle string authors similar to NPM + + if (value.isObject()) { + const auto contrib = value.toObject(); + return ExtensionContributor(contrib["name"].toString()); + } + + return ExtensionContributor(value.toString()); + } + } // namespace -ExtensionMetaData::ExtensionMetaData(QJsonObject const& jsonData) : m_JsonData{jsonData} +ExtensionContributor::ExtensionContributor(QString name) : m_Name{name} {} + +ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, + QJsonObject const& jsonData) + : m_JsonData{jsonData} { // read basic fields m_Identifier = jsonData["id"].toString(); m_Type = parseType(jsonData["type"].toString()); m_Name = jsonData["name"].toString(); + m_Author = parseContributor(jsonData["author"]); 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(""); + + if (jsonData.contains("icon")) { + m_Icon = QIcon(QDir(path).filePath(jsonData["icon"].toString())); + } + + if (jsonData.contains("contributors")) { + for (const auto& jsonContributor : jsonData["contributors"].toArray()) { + m_Contributors.push_back(parseContributor(jsonContributor)); + } + } } bool ExtensionMetaData::isValid() const @@ -142,7 +176,8 @@ ExtensionFactory::loadExtension(std::filesystem::path directory) return nullptr; } - return loadExtension(std::move(directory), ExtensionMetaData(jsonMetaData.object())); + return loadExtension(std::move(directory), + ExtensionMetaData(directory, jsonMetaData.object())); } std::unique_ptr @@ -318,9 +353,9 @@ PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData met 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 + // ["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 // From 8b073710fc44781b2a822fab915441b860319129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Mon, 31 Jul 2023 20:22:16 +0200 Subject: [PATCH 17/28] Default icon for extension. --- src/extension.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/extension.cpp b/src/extension.cpp index ffdb0260..f610fc60 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -80,7 +80,28 @@ ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, m_TranslationContext = jsonData["translationContext"].toString(""); if (jsonData.contains("icon")) { - m_Icon = QIcon(QDir(path).filePath(jsonData["icon"].toString())); + const QFileInfo icon{QDir(path), jsonData["icon"].toString()}; + if (icon.exists()) { + m_Icon = QIcon(icon.absoluteFilePath()); + } + } + + // TODO: move code in a better place or use a custom icon + if (m_Icon.isNull()) { + const QImage baseIcon(":/MO/gui/app_icon"); + QImage grayIcon = baseIcon.convertToFormat(QImage::Format_ARGB32); + { + for (int y = 0; y < grayIcon.height(); ++y) { + QRgb* scanLine = (QRgb*)grayIcon.scanLine(y); + for (int x = 0; x < grayIcon.width(); ++x) { + QRgb pixel = *scanLine; + uint ci = uint(qGray(pixel)); + *scanLine = qRgba(ci, ci, ci, qAlpha(pixel) / 3); + ++scanLine; + } + } + } + m_Icon = QIcon(QPixmap::fromImage(grayIcon)); } if (jsonData.contains("contributors")) { From 7d93e43d729e17a6316550ee38cf0c05f7eedec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Mon, 31 Jul 2023 20:22:26 +0200 Subject: [PATCH 18/28] Compute language name when missing for translations. --- src/extension.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/extension.cpp b/src/extension.cpp index f610fc60..bf0db9a9 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -308,16 +308,26 @@ TranslationExtension::parseTranslation(std::filesystem::path const& extensionFol 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()) { + if (qm_files.empty()) { return nullptr; } + const auto jsonName = jsonTranslation["name"]; + QString name; + if (jsonName.isString()) { + name = jsonName.toString(); + } else { + QLocale locale(identifier); + name = QString("%1 (%2)") + .arg(locale.nativeLanguageName()) + .arg(locale.nativeCountryName()); + } + return std::make_shared(identifier.toStdString(), name.toStdString(), std::move(qm_files)); } From f058a725d1283610afca8aa97428a628ac3421e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 29 Jun 2024 17:37:20 +0200 Subject: [PATCH 19/28] Cleaning for C++20/23. --- include/uibase/extension.h | 5 +++++ src/requirements.cpp | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/include/uibase/extension.h b/include/uibase/extension.h index 702b6730..83cb6633 100644 --- a/include/uibase/extension.h +++ b/include/uibase/extension.h @@ -137,11 +137,16 @@ class QDLLEXPORT IExtension // const auto& requirements() const { return m_Requirements; } +public: virtual ~IExtension() {} + IExtension& operator=(const IExtension&) = delete; protected: IExtension(std::filesystem::path path, ExtensionMetaData metadata); +public: + IExtension(const IExtension&) = default; + private: std::filesystem::path m_Path; ExtensionMetaData m_MetaData; diff --git a/src/requirements.cpp b/src/requirements.cpp index f911a149..08c0e77c 100644 --- a/src/requirements.cpp +++ b/src/requirements.cpp @@ -23,7 +23,7 @@ ExtensionRequirement::ExtensionRequirement( ExtensionRequirement::~ExtensionRequirement() = default; -bool ExtensionRequirement::check(IOrganizer* organizer) const +bool ExtensionRequirement::check([[maybe_unused]] IOrganizer* organizer) const { return true; } @@ -45,6 +45,7 @@ ExtensionRequirementFactory::parseRequirements(const ExtensionMetaData& metadata std::vector requirements; for (const auto& json_requirement : json_requirements.toArray()) { + // TODO } return requirements; From 10e9c7f1ec9882c9d0ad78a3d4d3824be00e6b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 29 Jun 2024 21:28:45 +0200 Subject: [PATCH 20/28] Add formatter for std::filesystem::path. --- include/uibase/formatters.h | 3 ++- include/uibase/formatters/path.h | 17 +++++++++++++++++ src/CMakeLists.txt | 13 +++++++------ 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 include/uibase/formatters/path.h diff --git a/include/uibase/formatters.h b/include/uibase/formatters.h index 78ee90fd..23e89905 100644 --- a/include/uibase/formatters.h +++ b/include/uibase/formatters.h @@ -1,6 +1,7 @@ #pragma once #include "./formatters/enums.h" +#include "./formatters/path.h" #include "./formatters/qt.h" #include "./formatters/random_access_containers.h" -#include "./formatters/strings.h" \ No newline at end of file +#include "./formatters/strings.h" diff --git a/include/uibase/formatters/path.h b/include/uibase/formatters/path.h new file mode 100644 index 00000000..d59050b6 --- /dev/null +++ b/include/uibase/formatters/path.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +template +struct std::formatter + : std::formatter +{ + template + FmtContext::iterator format(const std::filesystem::path& v, FmtContext& ctx) const + { + return std::formatter::format(v.native(), + ctx); + } +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f96f9e29..d5b23649 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,12 +34,12 @@ set(root_headers ../include/uibase/versioninfo.h ) set(extension_headers - ../include/uibase/extension.h - ../include/uibase/iextensionlist.h - ../include/uibase/ipluginloader.h - ../include/uibase/requirements.h - ../include/uibase/theme.h - ../include/uibase/translation.h + ../include/uibase/extensions/extension.h + ../include/uibase/extensions/iextensionlist.h + ../include/uibase/extensions/ipluginloader.h + ../include/uibase/extensions/requirements.h + ../include/uibase/extensions/theme.h + ../include/uibase/extensions/translation.h ) set(interface_headers ../include/uibase/ifiletree.h @@ -97,6 +97,7 @@ set(game_features_header set(formatters_header ../include/uibase/formatters/enums.h ../include/uibase/formatters/qt.h + ../include/uibase/formatters/path.h ../include/uibase/formatters/random_access_containers.h ../include/uibase/formatters/strings.h ../include/uibase/formatters.h From 39caa951f27fdff3b2d83f71e60daa5f6a5fc70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sun, 4 Aug 2024 18:52:09 +0200 Subject: [PATCH 21/28] Move extension-related headers to an extensions/ subfolder. --- include/uibase/{ => extensions}/extension.h | 0 include/uibase/{ => extensions}/iextensionlist.h | 0 include/uibase/{ => extensions}/ipluginloader.h | 0 include/uibase/{ => extensions}/requirements.h | 0 include/uibase/{ => extensions}/theme.h | 0 include/uibase/{ => extensions}/translation.h | 0 src/extension.cpp | 2 +- src/requirements.cpp | 6 +++--- src/theme.cpp | 2 +- 9 files changed, 5 insertions(+), 5 deletions(-) rename include/uibase/{ => extensions}/extension.h (100%) rename include/uibase/{ => extensions}/iextensionlist.h (100%) rename include/uibase/{ => extensions}/ipluginloader.h (100%) rename include/uibase/{ => extensions}/requirements.h (100%) rename include/uibase/{ => extensions}/theme.h (100%) rename include/uibase/{ => extensions}/translation.h (100%) diff --git a/include/uibase/extension.h b/include/uibase/extensions/extension.h similarity index 100% rename from include/uibase/extension.h rename to include/uibase/extensions/extension.h diff --git a/include/uibase/iextensionlist.h b/include/uibase/extensions/iextensionlist.h similarity index 100% rename from include/uibase/iextensionlist.h rename to include/uibase/extensions/iextensionlist.h diff --git a/include/uibase/ipluginloader.h b/include/uibase/extensions/ipluginloader.h similarity index 100% rename from include/uibase/ipluginloader.h rename to include/uibase/extensions/ipluginloader.h diff --git a/include/uibase/requirements.h b/include/uibase/extensions/requirements.h similarity index 100% rename from include/uibase/requirements.h rename to include/uibase/extensions/requirements.h diff --git a/include/uibase/theme.h b/include/uibase/extensions/theme.h similarity index 100% rename from include/uibase/theme.h rename to include/uibase/extensions/theme.h diff --git a/include/uibase/translation.h b/include/uibase/extensions/translation.h similarity index 100% rename from include/uibase/translation.h rename to include/uibase/extensions/translation.h diff --git a/src/extension.cpp b/src/extension.cpp index bf0db9a9..0b2f83eb 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -1,4 +1,4 @@ -#include "extension.h" +#include "extensions/extension.h" #include #include diff --git a/src/requirements.cpp b/src/requirements.cpp index 08c0e77c..894f56e7 100644 --- a/src/requirements.cpp +++ b/src/requirements.cpp @@ -1,8 +1,8 @@ -#include "requirements.h" +#include "extensions/requirements.h" #include -#include "extension.h" +#include "extensions/extension.h" #include "imoinfo.h" #include "log.h" @@ -49,4 +49,4 @@ ExtensionRequirementFactory::parseRequirements(const ExtensionMetaData& metadata } return requirements; -} \ No newline at end of file +} diff --git a/src/theme.cpp b/src/theme.cpp index 4431ebce..4d099863 100644 --- a/src/theme.cpp +++ b/src/theme.cpp @@ -1,4 +1,4 @@ -#include "theme.h" +#include "extensions/theme.h" #include "utility.h" From e4c47ea35fa83377f979eb9236bdac2348bdda6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Thu, 8 Aug 2024 11:59:20 +0200 Subject: [PATCH 22/28] Add version constraint system. --- .../uibase/extensions/versionconstraints.h | 70 ++++ src/CMakeLists.txt | 2 + src/versionconstraints.cpp | 305 ++++++++++++++++++ tests/test_versioning.cpp | 115 +++++++ 4 files changed, 492 insertions(+) create mode 100644 include/uibase/extensions/versionconstraints.h create mode 100644 src/versionconstraints.cpp diff --git a/include/uibase/extensions/versionconstraints.h b/include/uibase/extensions/versionconstraints.h new file mode 100644 index 00000000..a66cbb59 --- /dev/null +++ b/include/uibase/extensions/versionconstraints.h @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include + +#include "../versioning.h" + +namespace MOBase +{ +class InvalidConstraintException : public Exception +{ +public: + using Exception::Exception; +}; + +class VersionConstraintImpl; + +// class representing a version constraint, e.g. "2.3.*" or ">=2.4" +// +class QDLLEXPORT VersionConstraint +{ +public: + // wildcard placeholder for major/minor/patch/subpatch when constructing wildcard + // + static constexpr int WILDCARD = -1; + +public: + // parse a constraint from the given string + // + static VersionConstraint parse(QString const& value, Version::ParseMode mode); + +public: + // check if the given version matches this constraint + // + bool matches(Version const& version) const; + +public: + ~VersionConstraint(); + +private: + VersionConstraint(std::shared_ptr impl); + + std::shared_ptr m_Impl; +}; + +// class representing a set of version constraints, usually from dependency +// requirements e.g. "2.3.*", or ">= 2.4, <2.5" +// +class QDLLEXPORT VersionConstraints +{ +public: + // parse a set of constraints from the given string + // + static VersionConstraints parse(QString const& value, Version::ParseMode mode); + +public: + // construct a set of constraints + // + VersionConstraints(std::vector constraints); + + // check if the given version matches the set of constraints + // + bool matches(Version const& version) const; + +private: + std::vector m_Constraints; +}; + +} // namespace MOBase diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d5b23649..8e3379ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ set(extension_headers ../include/uibase/extensions/requirements.h ../include/uibase/extensions/theme.h ../include/uibase/extensions/translation.h + ../include/uibase/extensions/versionconstraints.h ) set(interface_headers ../include/uibase/ifiletree.h @@ -143,6 +144,7 @@ mo2_target_sources(uibase extension.cpp theme.cpp requirements.cpp + versionconstraints.cpp ) mo2_target_sources(uibase diff --git a/src/versionconstraints.cpp b/src/versionconstraints.cpp new file mode 100644 index 00000000..69f09be4 --- /dev/null +++ b/src/versionconstraints.cpp @@ -0,0 +1,305 @@ +#include "extensions/versionconstraints.h" + +#include "formatters.h" + +using VersionCompareFunction = bool (*)(MOBase::Version const& lhs, + MOBase::Version const& rhs); + +// official semver regex +static const QRegularExpression s_ConstraintStrictRegEx{ + R"(^(?P>=|<=|<|>|!=|==|\^|~)?\s*(?P0|[1-9*]\d*)(?:\.(?P0|[1-9*]\d*)(?:\.(?P0|[1-9*]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?)?)?$)"}; + +// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX +static const QRegularExpression s_ConstraintMO2RegEx{ + R"(^(?P>=|<=|<|>|!=|\^|~)?\s*(?P0|[1-9*]\d*)(?:\.(?P0|[1-9*]\d*)(?:\.(?P0|[1-9*]\d*)(?:\.(?P0|[1-9*]\d*))?(?:(?Pdev|a|alpha|b|beta|rc)(?P0|[1-9](?:[.0-9])*))?)?)?$)"}; + +// match from value to release type +static const std::unordered_map + s_StringToRelease{{"dev", MOBase::Version::Development}, + {"alpha", MOBase::Version::Alpha}, + {"a", MOBase::Version::Alpha}, + {"beta", MOBase::Version::Beta}, + {"b", MOBase::Version::Beta}, + {"rc", MOBase::Version::ReleaseCandidate}}; + +#define _COMPARE_PAIR(OP) \ + { \ + #OP, +[](MOBase::Version const& lhs, MOBase::Version const& rhs) { \ + return lhs OP rhs; \ + } \ + } + +static const std::unordered_map s_CompareToFunction{ + _COMPARE_PAIR(>), _COMPARE_PAIR(>=), _COMPARE_PAIR(<), + _COMPARE_PAIR(<=), _COMPARE_PAIR(!=), _COMPARE_PAIR(==)}; + +#undef _COMPARE_PAIR + +namespace MOBase +{ + +class VersionConstraintImpl +{ +public: + virtual bool matches(Version const& version) const = 0; + virtual ~VersionConstraintImpl() = default; +}; + +// version constraint for a range with lower bound included and upper bound excluded, +// typically used for tilde, caret and wilcard constraints +// +class RangeVersionConstraint : public VersionConstraintImpl +{ +public: + RangeVersionConstraint(Version const& min, Version const& max) + : m_Min{min}, m_Max{max} + {} + + bool matches(Version const& version) const override + { + return m_Min <= version && version < m_Max; + } + +private: + Version m_Min, m_Max; +}; + +// version constraint for inequality and equality constraint +// +class InequalityVersionConstraint : public VersionConstraintImpl +{ + +public: + InequalityVersionConstraint(Version const& target, VersionCompareFunction compare) + : m_Target{target}, m_Compare{compare} + {} + + bool matches(Version const& version) const override + { + return m_Compare(version, m_Target); + } + +private: + Version m_Target; + VersionCompareFunction m_Compare; +}; + +VersionConstraint VersionConstraint::parse(QString const& value, + Version::ParseMode mode) +{ + const auto& regex = mode == Version::ParseMode::SemVer ? s_ConstraintStrictRegEx + : s_ConstraintMO2RegEx; + + const auto match = regex.match(value); + if (!match.hasMatch()) { + throw InvalidConstraintException( + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); + } + + const auto constraint = match.captured("constraint"); + + const auto major_s = match.captured("major"); + const auto minor_s = match.captured("minor"); + const auto patch_s = match.captured("patch"); + const auto subpatch_s = match.captured("subpatch"); + + const auto wildcard = + major_s == "*" || minor_s == "*" || patch_s == "*" || subpatch_s == "*"; + const auto tilde = match.captured("constraint") == "~"; + const auto caret = match.captured("constraint") == "^"; + + // cannot use wildcard with a constraint + if (wildcard && !constraint.isEmpty()) { + throw InvalidConstraintException( + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); + } + + // cannot use pre-release with wilcard, tilde or caret constraint + if ((wildcard || tilde || caret) && match.hasCaptured("prerelease")) { + throw InvalidConstraintException( + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); + } + + // if a part has a wildcard, lower part should be missing or wildcard (e.g., 2.*.3 + // is invalid) + if (major_s == "*" && !minor_s.isEmpty() && minor_s != "*") { + throw InvalidConstraintException( + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); + } + if (minor_s == "*" && !patch_s.isEmpty() && patch_s != "*") { + throw InvalidConstraintException( + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); + } + if (patch_s == "*" && !subpatch_s.isEmpty() && subpatch_s != "*") { + throw InvalidConstraintException( + QString::fromStdString(std::format("invalid constraint string: '{}'", value))); + } + + std::vector> prereleases; + if (mode == Version::ParseMode::SemVer) { + for (auto& part : match.captured("prerelease") + .split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) { + // try to extract an int + bool ok = true; + const auto intValue = part.toInt(&ok); + if (ok) { + prereleases.push_back(intValue); + continue; + } + + // check if we have a valid prerelease type + const auto it = s_StringToRelease.find(part.toLower()); + if (it == s_StringToRelease.end()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid prerelease type: '{}'", part))); + } + + prereleases.push_back(it->second); + } + } else { + prereleases.push_back(s_StringToRelease.at(match.captured("type"))); + + // for version with decimal point, e.g., 2.4.1rc1.1, we split the components into + // pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc, + // 1, 1} + // + for (const auto& preVersion : + match.captured("prerelease").split(".", Qt::SkipEmptyParts)) { + prereleases.push_back(preVersion.toInt()); + } + } + + constexpr auto min_int = std::numeric_limits::min(); + constexpr auto max_int = std::numeric_limits::max(); + + std::shared_ptr impl; + + if (wildcard || caret || tilde) { + + // you can get more information at + // https://python-poetry.org/docs/dependency-specification/ + + // note that the only case where all 4 xxxOk is false is for '*' + // + bool majorOk, minorOk, patchOk, subpatchOk; + auto major = major_s.toInt(&majorOk), minor = minor_s.toInt(&minorOk), + patch = patch_s.toInt(&patchOk), subpatch = subpatch_s.toInt(&subpatchOk); + + // the lower bound is always the actual version with missing or wildcard components + // set to 0, e.g. + // - 2.3.* -> >= 2.3.0 + // - ^1 -> >= 1.0.0 + // - ^0.3 -> >= 0.3.0 + // - ~1.2 -> >= 1.2.0 + const Version min = Version(major, minor, patch, subpatch); + + // the upper bound is a bit more complicated to compute + Version max = Version(max_int, max_int, max_int, max_int); + + if (wildcard) { + // for wildcard, we increment the last non-wildcard character by one + // + if (majorOk && minorOk && patchOk) { + max = Version(major, minor, patch + 1); + } else if (majorOk && minorOk) { + max = Version(major, minor + 1, 0); + } else if (majorOk) { + max = Version(major + 1, 0, 0); + } else { + max = Version(max_int, max_int, max_int, max_int); + } + } else if (caret) { + // TODO: clean this... + + if (!minorOk && !patchOk && !subpatchOk) { + max = Version(major + 1, 0, 0); + } else if (!patchOk && !subpatchOk) { + if (major == 0) { + max = Version(major, minor + 1, 0); + } else { + max = Version(major + 1, 0, 0); + } + } else if (!subpatchOk) { + if (major == 0 && minor == 0) { + max = Version(major, minor, patch + 1); + } else if (major == 0) { + max = Version(major, minor + 1, 0); + } else { + max = Version(major + 1, 0, 0); + } + } else { + if (major == 0 && minor == 0 && patch == 0 && subpatch == 0) { + max = min; // this creates an impossible range (>= 0, < 0), but is expected + } else if (major == 0 && minor == 0 && patch == 0) { + max = Version(major, minor, patch, subpatch + 1); + } else if (major == 0 && minor == 0) { + max = Version(major, minor, patch + 1, 0); + } else if (major == 0) { + max = Version(major, minor + 1, 0); + } else { + max = Version(major + 1, 0, 0); + } + } + + } else if (tilde) { + if (minorOk && patchOk && subpatchOk) { + max = Version(major, minor, patch, subpatch + 1); + } else if (minorOk && patchOk) { + max = Version(major, minor, patch + 1); + } else if (minorOk) { + max = Version(major, minor + 1, 0); + } else { + max = Version(major + 1, 0, 0); + } + } + + impl = std::make_shared(min, max); + + } else { + auto op = match.captured("constraint"); + if (op.isEmpty()) { + op = "=="; + } + impl = std::make_shared( + Version(major_s.toInt(), minor_s.toInt(), patch_s.toInt(), subpatch_s.toInt(), + std::move(prereleases)), + s_CompareToFunction.at(op)); + } + + return VersionConstraint(std::move(impl)); +} + +VersionConstraint::VersionConstraint(std::shared_ptr impl) + : m_Impl{std::move(impl)} +{} + +VersionConstraint::~VersionConstraint() = default; + +bool VersionConstraint::matches(Version const& version) const +{ + return m_Impl->matches(version); +} + +VersionConstraints VersionConstraints::parse(QString const& value, + Version::ParseMode mode) +{ + std::vector constraints; + for (const auto& part : value.split(",")) { + constraints.push_back(VersionConstraint::parse(part.trimmed(), mode)); + } + return VersionConstraints(std::move(constraints)); +} + +bool VersionConstraints::matches(Version const& version) const +{ + return std::all_of(m_Constraints.begin(), m_Constraints.end(), + [version](const auto& constraint) { + return constraint.matches(version); + }); +} + +VersionConstraints::VersionConstraints(std::vector checkers) + : m_Constraints{std::move(checkers)} +{} + +} // namespace MOBase diff --git a/tests/test_versioning.cpp b/tests/test_versioning.cpp index 08a346fd..e68579d0 100644 --- a/tests/test_versioning.cpp +++ b/tests/test_versioning.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -88,3 +89,117 @@ TEST(VersioningTest, VersionCompare) v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0, Alpha)); } + +TEST(VersioningTest, VersionConstraintTest) +{ + // shortcut + using v = Version; + + constexpr auto MAX = std::numeric_limits::max(); + + auto check = [](const QString& constraint, Version const& v, + Version::ParseMode mode = Version::ParseMode::SemVer) { + return VersionConstraint::parse(constraint, mode).matches(v); + }; + + // inequality + + ASSERT_TRUE(check("2.5.2", v(2, 5, 2))); + ASSERT_FALSE(check("2.5.3", v(2, 5, 2))); + + ASSERT_TRUE(check(">2.5", v(2, 5, 1))); + ASSERT_TRUE(check(">2.5.2", v(2, 5, 3))); + ASSERT_FALSE(check(">2.5.2", v(2, 5, 2))); + ASSERT_FALSE(check(">2.5.3", v(2, 5, 2))); + + ASSERT_TRUE(check("<2.5", v(2, 4, MAX))); + + // wilcard + + ASSERT_TRUE(check("*", v(2, 4, MAX))); + + ASSERT_TRUE(check("2.4.*", v(2, 4, 0))); + ASSERT_TRUE(check("2.4.*", v(2, 4, MAX))); + ASSERT_FALSE(check("2.4.*", v(2, 3, MAX))); + ASSERT_FALSE(check("2.4.*", v(2, 5, 0))); + + // caret + + ASSERT_TRUE(check("^1.2.3", v(1, 2, 3))); + ASSERT_TRUE(check("^1.2.3", v(1, 2, 4))); + ASSERT_TRUE(check("^1.2.3", v(1, 3, 1))); + ASSERT_TRUE(check("^1.2.3", v(1, MAX, 5))); + ASSERT_FALSE(check("^1.2.3", v(1, 2, 2, MAX))); + ASSERT_FALSE(check("^1.2.3", v(1, 1, 0))); + ASSERT_FALSE(check("^1.2.3", v(2, 0, 0))); + + ASSERT_TRUE(check("^1.2", v(1, 2, 0))); + ASSERT_TRUE(check("^1.2", v(1, 2, 4))); + ASSERT_TRUE(check("^1.2", v(1, 3, 1))); + ASSERT_TRUE(check("^1.2", v(1, 9, 5))); + ASSERT_FALSE(check("^1.2", v(1, 1, MAX))); + ASSERT_FALSE(check("^1.2", v(1, 1, 0))); + ASSERT_FALSE(check("^1.2", v(2, 0, 0))); + + ASSERT_TRUE(check("^1", v(1, 0, 0))); + ASSERT_TRUE(check("^1", v(1, 2, 4))); + ASSERT_TRUE(check("^1", v(1, 3, 1))); + ASSERT_TRUE(check("^1", v(1, 9, 5))); + ASSERT_FALSE(check("^1", v(0, MAX, MAX))); + ASSERT_FALSE(check("^1", v(0, MAX, 0))); + ASSERT_FALSE(check("^1", v(2, 0, 0))); + + ASSERT_TRUE(check("^0.2.3", v(0, 2, 3))); + ASSERT_TRUE(check("^0.2.3", v(0, 2, MAX))); + ASSERT_FALSE(check("^0.2.3", v(0, 1, MAX))); + ASSERT_FALSE(check("^0.2.3", v(0, 3, 0))); + + ASSERT_TRUE(check("^0.0.3", v(0, 0, 3))); + ASSERT_TRUE(check("^0.0.3", v(0, 0, 3, MAX))); + ASSERT_FALSE(check("^0.0.3", v(0, 0, 2, MAX))); + ASSERT_FALSE(check("^0.0.3", v(0, 0, 4))); + + ASSERT_TRUE(check("^0.0", v(0, 0, 0))); + ASSERT_TRUE(check("^0.0", v(0, 0, MAX))); + ASSERT_FALSE(check("^0.0", v(0, 1, 0))); + + ASSERT_TRUE(check("^0", v(0, 0, 0))); + ASSERT_TRUE(check("^0", v(0, MAX, MAX, MAX))); + ASSERT_FALSE(check("^0", v(1, 0, 0))); + + // tilde + + ASSERT_TRUE(check("~1.2.3", v(1, 2, 3))); + ASSERT_TRUE(check("~1.2.3", v(1, 2, 3, MAX))); + ASSERT_FALSE(check("~1.2.3", v(1, 2, 2, MAX))); + ASSERT_FALSE(check("~1.2.3", v(1, 3, 0))); + + ASSERT_TRUE(check("~1.2", v(1, 2, 0))); + ASSERT_TRUE(check("~1.2", v(1, 2, MAX, MAX))); + ASSERT_FALSE(check("~1.2", v(1, 1, MAX, MAX))); + ASSERT_FALSE(check("~1.2", v(1, 3, 0))); + + ASSERT_TRUE(check("~1", v(1, 0, 0))); + ASSERT_TRUE(check("~1", v(1, MAX, MAX, MAX))); + ASSERT_FALSE(check("~1", v(0, MAX, MAX, MAX))); + ASSERT_FALSE(check("~1", v(2, 0, 0))); +} + +TEST(VersioningTest, VersionConstraintsTest) +{ + // shortcut + using v = Version; + + auto check = [](const QString& constraints, Version const& v, + Version::ParseMode mode = Version::ParseMode::SemVer) { + return VersionConstraints::parse(constraints, mode).matches(v); + }; + + ASSERT_TRUE(check("2.5.2", v(2, 5, 2))); + ASSERT_FALSE(check("2.5.3", v(2, 5, 2))); + + ASSERT_TRUE(check(">=2.5.0, <2.6.0", v(2, 5, 2))); + ASSERT_FALSE(check(">=2.5.0, <2.6.0", v(2, 6, 0))); + ASSERT_FALSE(check(">=2.5.0, <2.6.0", v(2, 5, 0, Development))); + ASSERT_FALSE(check(">=2.5.0, <2.6.0", v(2, 4, 4))); +} \ No newline at end of file From 0367e32eac019aa684b7245a5fa0efa655b976e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Fri, 9 Aug 2024 11:14:24 +0200 Subject: [PATCH 23/28] Implement requirements for extension. --- include/uibase/exceptions.h | 2 + include/uibase/extensions/extension.h | 72 ++++---- include/uibase/extensions/requirements.h | 28 ++- include/uibase/extensions/theme.h | 2 +- include/uibase/extensions/translation.h | 2 +- .../uibase/extensions/versionconstraints.h | 7 +- include/uibase/versioning.h | 3 +- src/CMakeLists.txt | 1 - src/extension.cpp | 138 +++++++------- src/requirements.cpp | 170 ++++++++++++++++-- src/uibase_en.ts | 1 - src/versionconstraints.cpp | 16 +- tests/CMakeLists.txt | 1 + tests/test_extensions.cpp | 54 ++++++ 14 files changed, 372 insertions(+), 125 deletions(-) create mode 100644 tests/test_extensions.cpp diff --git a/include/uibase/exceptions.h b/include/uibase/exceptions.h index c89a4833..b6425675 100644 --- a/include/uibase/exceptions.h +++ b/include/uibase/exceptions.h @@ -20,6 +20,8 @@ namespace MOBase class QDLLEXPORT Exception : public std::exception { public: + Exception(const char* text) : m_Message(text) {} + Exception(const std::string& text) : m_Message(QByteArray::fromStdString(text)) {} Exception(const QString& text) : m_Message(text.toUtf8()) {} virtual const char* what() const noexcept override { return m_Message.constData(); } diff --git a/include/uibase/extensions/extension.h b/include/uibase/extensions/extension.h index 83cb6633..c26f2bad 100644 --- a/include/uibase/extensions/extension.h +++ b/include/uibase/extensions/extension.h @@ -8,20 +8,25 @@ #include #include -#include "dllimport.h" -#include "iplugingame.h" +#include "../dllimport.h" +#include "../iplugingame.h" +#include "../versioning.h" #include "requirements.h" #include "theme.h" #include "translation.h" -#include "versioninfo.h" namespace MOBase { class IExtension; +class InvalidExtensionMetaDataException : public Exception +{ +public: + using Exception::Exception; +}; + enum class ExtensionType { - INVALID, THEME, TRANSLATION, PLUGIN, @@ -48,10 +53,6 @@ class QDLLEXPORT ExtensionContributor class QDLLEXPORT ExtensionMetaData { public: - // check if that metadata object is valid - // - bool isValid() const; - // retrieve the identifier of the extension // const auto& identifier() const { return m_Identifier; } @@ -72,7 +73,7 @@ class QDLLEXPORT ExtensionMetaData // auto type() const { return m_Type; } - // retrieve the description of the extension. + // retrieve the description of the extension // auto description() const { return localized(m_Description); } @@ -80,10 +81,14 @@ class QDLLEXPORT ExtensionMetaData // const auto& icon() const { return m_Icon; } - // retrieve the version of the extension. + // retrieve the version of the extension // const auto& version() const { return m_Version; } + // retrieve the requirements of the extension + // + const auto& requirements() const { return m_Requirements; } + // retrieve the raw JSON metadata, this is mostly useful for specific extension type // to extract custom parts // @@ -92,8 +97,8 @@ class QDLLEXPORT ExtensionMetaData // retrieve the content objects of the extension QJsonObject content() const; -private: - QString localized(QString const& value) const; +protected: + ExtensionMetaData(std::filesystem::path const& path, const QJsonObject& jsonData); private: friend class ExtensionFactory; @@ -101,9 +106,7 @@ class QDLLEXPORT ExtensionMetaData constexpr static const char* DEFAULT_TRANSLATIONS_FOLDER = "translations"; constexpr static const char* DEFAULT_STYLESHEET_PATH = "stylesheets"; - ExtensionType parseType(QString const& value) const; - - ExtensionMetaData(std::filesystem::path const& path, const QJsonObject& jsonData); + std::optional parseType(QString const& value) const; private: QJsonObject m_JsonData; @@ -116,10 +119,13 @@ class QDLLEXPORT ExtensionMetaData ExtensionType m_Type; QString m_Description; QIcon m_Icon; - VersionInfo m_Version; + Version m_Version; + std::vector m_Requirements; std::filesystem::path m_TranslationFilesPrefix; std::filesystem::path m_StyleSheetFilePath; + + QString localized(QString const& value) const; }; class QDLLEXPORT IExtension @@ -133,16 +139,12 @@ class QDLLEXPORT IExtension // const auto& metadata() const { return m_MetaData; } - // retrieve the requirements of the extension - // - const auto& requirements() const { return m_Requirements; } - public: virtual ~IExtension() {} IExtension& operator=(const IExtension&) = delete; protected: - IExtension(std::filesystem::path path, ExtensionMetaData metadata); + IExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); public: IExtension(const IExtension&) = default; @@ -150,7 +152,6 @@ class QDLLEXPORT IExtension private: std::filesystem::path m_Path; ExtensionMetaData m_MetaData; - std::vector m_Requirements; }; // factory for extensions @@ -161,13 +162,14 @@ class QDLLEXPORT ExtensionFactory // 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); + static std::unique_ptr + loadExtension(std::filesystem::path const& directory); private: // load an extension from the given directory // - static std::unique_ptr loadExtension(std::filesystem::path directory, - ExtensionMetaData metadata); + static std::unique_ptr + loadExtension(std::filesystem::path const& directory, ExtensionMetaData&& metadata); }; // theme extension that provides one or more base themes for MO2 @@ -180,12 +182,12 @@ class QDLLEXPORT ThemeExtension : public IExtension const auto& themes() const { return m_Themes; } private: - ThemeExtension(std::filesystem::path path, ExtensionMetaData metadata, + ThemeExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata, std::vector> themes); friend class ExtensionFactory; - static std::unique_ptr loadExtension(std::filesystem::path path, - ExtensionMetaData metadata); + static std::unique_ptr + loadExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); static std::shared_ptr parseTheme(std::filesystem::path const& extensionFolder, const QString& identifier, @@ -205,12 +207,12 @@ class QDLLEXPORT TranslationExtension : public IExtension const auto& translations() const { return m_Translations; } private: - TranslationExtension(std::filesystem::path path, ExtensionMetaData metadata, + TranslationExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata, std::vector> translations); friend class ExtensionFactory; static std::unique_ptr - loadExtension(std::filesystem::path path, ExtensionMetaData metadata); + loadExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); static std::shared_ptr parseTranslation(std::filesystem::path const& extensionFolder, @@ -241,14 +243,14 @@ class QDLLEXPORT PluginExtension : public IExtension protected: PluginExtension( - std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::filesystem::path const& 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); + static std::unique_ptr + loadExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); private: // auto-detect plugins @@ -271,8 +273,8 @@ class QDLLEXPORT GameExtension : public PluginExtension GameExtension(PluginExtension&& pluginExtension); friend class ExtensionFactory; - static std::unique_ptr loadExtension(std::filesystem::path path, - ExtensionMetaData metadata); + static std::unique_ptr loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata); }; } // namespace MOBase diff --git a/include/uibase/extensions/requirements.h b/include/uibase/extensions/requirements.h index 1d71d52d..2a10d5de 100644 --- a/include/uibase/extensions/requirements.h +++ b/include/uibase/extensions/requirements.h @@ -6,13 +6,26 @@ #include #include -#include "dllimport.h" +#include "../dllimport.h" +#include "../exceptions.h" namespace MOBase { class IOrganizer; class ExtensionMetaData; +class InvalidRequirementException : public Exception +{ +public: + using Exception::Exception; +}; + +class InvalidRequirementsException : public Exception +{ +public: + using Exception::Exception; +}; + class ExtensionRequirementImpl; // extension requirements @@ -44,13 +57,22 @@ class QDLLEXPORT ExtensionRequirement // bool check(IOrganizer* organizer) const; + // retrieve the type of this extension + // + Type type() const; + + // retrieve a textual representation of this requirement, e.g. "ModOrganizer 2.5.4" + // for a requirement that requires MO2 2.5.4 + // + QString string() const; + +public: ~ExtensionRequirement(); private: friend class ExtensionRequirementFactory; ExtensionRequirement(std::shared_ptr impl); - std::shared_ptr m_Impl; }; @@ -62,7 +84,7 @@ class QDLLEXPORT ExtensionRequirementFactory // extract requirements from the given metadata // static std::vector - parseRequirements(const ExtensionMetaData& metadata); + parseRequirements(const QJsonValue& json_requirements); private: }; diff --git a/include/uibase/extensions/theme.h b/include/uibase/extensions/theme.h index 624f1a01..1802ca14 100644 --- a/include/uibase/extensions/theme.h +++ b/include/uibase/extensions/theme.h @@ -6,7 +6,7 @@ #include -#include "dllimport.h" +#include "../dllimport.h" namespace MOBase { diff --git a/include/uibase/extensions/translation.h b/include/uibase/extensions/translation.h index b1dd66f9..dafb3371 100644 --- a/include/uibase/extensions/translation.h +++ b/include/uibase/extensions/translation.h @@ -4,7 +4,7 @@ #include #include -#include "dllimport.h" +#include "../dllimport.h" namespace MOBase { diff --git a/include/uibase/extensions/versionconstraints.h b/include/uibase/extensions/versionconstraints.h index a66cbb59..61791fcb 100644 --- a/include/uibase/extensions/versionconstraints.h +++ b/include/uibase/extensions/versionconstraints.h @@ -57,13 +57,18 @@ class QDLLEXPORT VersionConstraints public: // construct a set of constraints // - VersionConstraints(std::vector constraints); + VersionConstraints(QString const& repr, std::vector constraints); // check if the given version matches the set of constraints // bool matches(Version const& version) const; + // retrieve a string representation of this set of constraints + // + auto string() const { return m_Repr; } + private: + QString m_Repr; std::vector m_Constraints; }; diff --git a/include/uibase/versioning.h b/include/uibase/versioning.h index c4f95419..ab3d7b14 100644 --- a/include/uibase/versioning.h +++ b/include/uibase/versioning.h @@ -76,7 +76,8 @@ class QDLLEXPORT Version // do not add metadata even if present // - NoMetadata = 0b1000 + NoMetadata = 0b1000, + }; Q_DECLARE_FLAGS(FormatModes, FormatMode); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e3379ec..ebd77919 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -117,7 +117,6 @@ mo2_target_sources(uibase errorcodes.cpp eventfilter.cpp executableinfo.cpp - extension.cpp filesystemutilities.cpp guessedvalue.cpp json.cpp diff --git a/src/extension.cpp b/src/extension.cpp index 0b2f83eb..761db63a 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -65,15 +65,39 @@ ExtensionContributor::ExtensionContributor(QString name) : m_Name{name} {} ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, QJsonObject const& jsonData) - : m_JsonData{jsonData} + : m_JsonData{jsonData}, m_Version{0, 0, 0}, m_Requirements{} { // read basic fields - m_Identifier = jsonData["id"].toString(); - m_Type = parseType(jsonData["type"].toString()); - m_Name = jsonData["name"].toString(); + m_Identifier = jsonData["id"].toString(); + if (m_Identifier.isEmpty()) { + throw InvalidExtensionMetaDataException("missing identifier"); + } + + { + const auto maybeType = parseType(jsonData["type"].toString()); + if (!maybeType.has_value()) { + throw InvalidExtensionMetaDataException( + std::format("invalid or missing type '{}'", jsonData["type"].toString())); + } + + m_Type = *maybeType; + } + + m_Name = jsonData["name"].toString(); + if (m_Name.isEmpty()) { + throw InvalidExtensionMetaDataException("missing name"); + } + m_Author = parseContributor(jsonData["author"]); m_Description = jsonData["description"].toString(); - m_Version.parse(jsonData["version"].toString("0.0.0")); + + try { + m_Version = Version::parse(jsonData["version"].toString("0.0.0"), + Version::ParseMode::SemVer); + } catch (InvalidVersionException const& ex) { + throw InvalidExtensionMetaDataException( + std::format("invalid or missing version '{}'", jsonData["version"].toString())); + } // TODO: name of the key // translation context @@ -86,38 +110,25 @@ ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, } } - // TODO: move code in a better place or use a custom icon - if (m_Icon.isNull()) { - const QImage baseIcon(":/MO/gui/app_icon"); - QImage grayIcon = baseIcon.convertToFormat(QImage::Format_ARGB32); - { - for (int y = 0; y < grayIcon.height(); ++y) { - QRgb* scanLine = (QRgb*)grayIcon.scanLine(y); - for (int x = 0; x < grayIcon.width(); ++x) { - QRgb pixel = *scanLine; - uint ci = uint(qGray(pixel)); - *scanLine = qRgba(ci, ci, ci, qAlpha(pixel) / 3); - ++scanLine; - } - } - } - m_Icon = QIcon(QPixmap::fromImage(grayIcon)); - } - if (jsonData.contains("contributors")) { for (const auto& jsonContributor : jsonData["contributors"].toArray()) { m_Contributors.push_back(parseContributor(jsonContributor)); } } -} -bool ExtensionMetaData::isValid() const -{ - return !m_Identifier.isEmpty() && !m_Name.isEmpty() && m_Version.isValid() && - m_Type != ExtensionType::INVALID; + if (jsonData.contains("requirements")) { + try { + m_Requirements = + ExtensionRequirementFactory::parseRequirements(jsonData["requirements"]); + } catch (InvalidRequirementException const& ex) { + throw InvalidExtensionMetaDataException(ex.what()); + } catch (InvalidRequirementsException const& ex) { + throw InvalidExtensionMetaDataException(ex.what()); + } + } } -ExtensionType ExtensionMetaData::parseType(QString const& value) const +std::optional ExtensionMetaData::parseType(QString const& value) const { std::map stringToTypes{ {"theme", ExtensionType::THEME}, @@ -125,7 +136,7 @@ ExtensionType ExtensionMetaData::parseType(QString const& value) const {"plugin", ExtensionType::PLUGIN}, {"game", ExtensionType::GAME}}; - auto type = ExtensionType::INVALID; + std::optional type; for (auto& [k, v] : stringToTypes) { if (k.compare(value, Qt::CaseInsensitive) == 0) { type = v; @@ -163,13 +174,12 @@ QJsonObject ExtensionMetaData::content() const return value.toObject(); } -IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata) - : m_Path{std::move(path)}, m_MetaData{std::move(metadata)}, - m_Requirements{ExtensionRequirementFactory::parseRequirements(m_MetaData)} +IExtension::IExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata) + : m_Path{path}, m_MetaData{std::move(metadata)} {} std::unique_ptr -ExtensionFactory::loadExtension(std::filesystem::path directory) +ExtensionFactory::loadExtension(std::filesystem::path const& directory) { const auto metadataPath = directory / METADATA_FILENAME; @@ -197,44 +207,44 @@ ExtensionFactory::loadExtension(std::filesystem::path directory) return nullptr; } - return loadExtension(std::move(directory), - ExtensionMetaData(directory, jsonMetaData.object())); + try { + return loadExtension(directory, + ExtensionMetaData(directory, jsonMetaData.object())); + } catch (InvalidExtensionMetaDataException const& ex) { + log::warn("failed to load extension from '{}': invalid metadata ({})", + directory.native(), ex.what()); + return nullptr; + } } std::unique_ptr -ExtensionFactory::loadExtension(std::filesystem::path directory, - ExtensionMetaData metadata) +ExtensionFactory::loadExtension(std::filesystem::path const& 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)); + return ThemeExtension::loadExtension(directory, std::move(metadata)); case ExtensionType::TRANSLATION: - return TranslationExtension::loadExtension(std::move(directory), - std::move(metadata)); + return TranslationExtension::loadExtension(directory, std::move(metadata)); case ExtensionType::PLUGIN: - return PluginExtension::loadExtension(std::move(directory), std::move(metadata)); + return PluginExtension::loadExtension(directory, std::move(metadata)); case ExtensionType::GAME: - return GameExtension::loadExtension(std::move(directory), std::move(metadata)); - case ExtensionType::INVALID: + return GameExtension::loadExtension(directory, std::move(metadata)); default: log::warn("failed to load extension from '{}': invalid type", directory.native()); return nullptr; } } -ThemeExtension::ThemeExtension(std::filesystem::path path, ExtensionMetaData metadata, +ThemeExtension::ThemeExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata, std::vector> themes) - : IExtension{std::move(path), std::move(metadata)}, m_Themes{std::move(themes)} + : IExtension{path, std::move(metadata)}, m_Themes{std::move(themes)} {} std::unique_ptr -ThemeExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) +ThemeExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { std::vector> themes; const auto& jsonThemes = metadata.content()["themes"].toObject(); @@ -253,7 +263,7 @@ ThemeExtension::loadExtension(std::filesystem::path path, ExtensionMetaData meta } return std::unique_ptr{ - new ThemeExtension(path, metadata, std::move(themes))}; + new ThemeExtension(path, std::move(metadata), std::move(themes))}; } std::shared_ptr @@ -273,15 +283,15 @@ ThemeExtension::parseTheme(std::filesystem::path const& extensionFolder, } TranslationExtension::TranslationExtension( - std::filesystem::path path, ExtensionMetaData metadata, + std::filesystem::path const& 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) +TranslationExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { std::vector> translations; const auto& jsonTranslations = metadata.content()["translations"].toObject(); @@ -300,7 +310,7 @@ TranslationExtension::loadExtension(std::filesystem::path path, } return std::unique_ptr{ - new TranslationExtension(path, metadata, std::move(translations))}; + new TranslationExtension(path, std::move(metadata), std::move(translations))}; } std::shared_ptr @@ -333,17 +343,18 @@ TranslationExtension::parseTranslation(std::filesystem::path const& extensionFol } PluginExtension::PluginExtension( - std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::filesystem::path const& path, ExtensionMetaData&& metadata, bool autodetect, std::map plugins, std::vector> themeAdditions, std::vector> translationAdditions) - : IExtension(std::move(path), std::move(metadata)), m_AutoDetect{autodetect}, + : IExtension(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) +PluginExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { namespace fs = std::filesystem; @@ -450,8 +461,9 @@ GameExtension::GameExtension(PluginExtension&& pluginExtension) : PluginExtension(std::move(pluginExtension)) {} -std::unique_ptr GameExtension::loadExtension(std::filesystem::path path, - ExtensionMetaData metadata) +std::unique_ptr +GameExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { auto extension = PluginExtension::loadExtension(std::move(path), std::move(metadata)); return extension diff --git a/src/requirements.cpp b/src/requirements.cpp index 894f56e7..11bd81c5 100644 --- a/src/requirements.cpp +++ b/src/requirements.cpp @@ -3,6 +3,8 @@ #include #include "extensions/extension.h" +#include "extensions/iextensionlist.h" +#include "extensions/versionconstraints.h" #include "imoinfo.h" #include "log.h" @@ -10,7 +12,91 @@ namespace MOBase { class ExtensionRequirementImpl -{}; +{ +public: + using Type = ExtensionRequirement::Type; + +public: + virtual bool check(IOrganizer* organizer) const = 0; + virtual Type type() const = 0; + virtual QString string() const = 0; + virtual ~ExtensionRequirementImpl() = default; +}; + +// requirement for the version of MO2 itself +// +class CoreVersionExtensionRequirement : public ExtensionRequirementImpl +{ +public: + CoreVersionExtensionRequirement(VersionConstraints const& constraints) + : m_Constraints{constraints} + {} + + bool check(IOrganizer* organizer) const override + { + return m_Constraints.matches(organizer->version()); + } + + Type type() const override { return Type::VERSION; } + + QString string() const override + { + return QString("ModOrganizer2 %1").arg(m_Constraints.string()); + } + +private: + VersionConstraints m_Constraints; +}; + +// requirement for another extension +// +class DependencyExtensionRequirement : public ExtensionRequirementImpl +{ +public: + DependencyExtensionRequirement(QString const& extension, + VersionConstraints const& constraints) + : m_Extension{extension}, m_Constraints{constraints} + {} + + bool check(IOrganizer* organizer) const override + { + return organizer->extensionList().enabled(m_Extension) && + m_Constraints.matches( + organizer->extensionList().get(m_Extension).metadata().version()); + } + + Type type() const override { return Type::DEPENDENCY; } + + QString string() const override + { + return QString("%1 %2").arg(m_Extension, m_Constraints.string()); + } + +private: + QString m_Extension; + VersionConstraints m_Constraints; +}; + +// requirement for games +// +class GameExtensionRequirement : public ExtensionRequirementImpl +{ +public: + GameExtensionRequirement(QStringList const& games) : m_Games{games} {} + + bool check(IOrganizer* organizer) const override + { + return organizer->managedGame() && + m_Games.contains(organizer->managedGame()->gameName()); + } + + Type type() const override { return Type::GAME; } + + QString string() const override { return m_Games.join(", "); } + +private: + QStringList m_Games; +}; } // namespace MOBase @@ -23,29 +109,89 @@ ExtensionRequirement::ExtensionRequirement( ExtensionRequirement::~ExtensionRequirement() = default; -bool ExtensionRequirement::check([[maybe_unused]] IOrganizer* organizer) const +bool ExtensionRequirement::check(IOrganizer* organizer) const { - return true; + return m_Impl->check(organizer); } -std::vector -ExtensionRequirementFactory::parseRequirements(const ExtensionMetaData& metadata) +ExtensionRequirement::Type ExtensionRequirement::type() const +{ + return m_Impl->type(); +} + +QString ExtensionRequirement::string() const +{ + return m_Impl->string(); +} + +namespace +{ +std::optional parseType(QString const& value) { - if (!metadata.json().contains("requirements")) { - return {}; + std::map stringToTypes{ + {"game", ExtensionRequirement::Type::GAME}, + {"extension", ExtensionRequirement::Type::DEPENDENCY}, + {"version", ExtensionRequirement::Type::VERSION}}; + + std::optional type; + for (auto& [k, v] : stringToTypes) { + if (k.compare(value, Qt::CaseInsensitive) == 0) { + type = v; + break; + } } - const auto json_requirements = metadata.json()["requirements"]; + return type; +} +} // namespace +std::vector +ExtensionRequirementFactory::parseRequirements(const QJsonValue& json_requirements) +{ if (!json_requirements.isArray()) { - log::warn("expected array of requirements for extension '{}', found '{}'", - metadata.identifier(), json_requirements.type()); - return {}; + throw InvalidRequirementsException("expected an array of requirements"); } std::vector requirements; for (const auto& json_requirement : json_requirements.toArray()) { - // TODO + if (!json_requirement.isObject()) { + throw InvalidRequirementException("invalid requirement"); + } + + auto json_object = json_requirement.toObject(); + + const auto type = parseType(json_object["type"].toString()); + if (!type.has_value()) { + throw InvalidRequirementException("missing requirement type"); + } + + try { + switch (*type) { + case ExtensionRequirement::Type::GAME: + if (!json_object.contains("games") || !json_object["games"].isArray()) { + throw InvalidRequirementException("invalid requirement"); + } + requirements.push_back( + ExtensionRequirement(std::make_shared( + json_object["games"].toVariant().toStringList()))); + break; + case ExtensionRequirement::Type::DEPENDENCY: + requirements.push_back( + ExtensionRequirement(std::make_shared( + json_object["extension"].toString(), + VersionConstraints::parse(json_object["version"].toString(), + Version::ParseMode::SemVer)))); + break; + case ExtensionRequirement::Type::VERSION: + requirements.push_back(ExtensionRequirement( + std::make_shared(VersionConstraints::parse( + json_object["version"].toString(), Version::ParseMode::MO2)))); + break; + } + } catch (InvalidConstraintException const& ex) { + throw InvalidRequirementException( + std::format("invalid requirement constraints: {}", ex.what())); + } } return requirements; diff --git a/src/uibase_en.ts b/src/uibase_en.ts index d5dd17a5..1156e86c 100644 --- a/src/uibase_en.ts +++ b/src/uibase_en.ts @@ -181,7 +181,6 @@ Failed to save '%1', could not create a temporary file: %2 (error %3) - Failed to save '{}', could not create a temporary file: {} (error {}) diff --git a/src/versionconstraints.cpp b/src/versionconstraints.cpp index 69f09be4..1cfc9dd7 100644 --- a/src/versionconstraints.cpp +++ b/src/versionconstraints.cpp @@ -169,7 +169,6 @@ VersionConstraint VersionConstraint::parse(QString const& value, } } - constexpr auto min_int = std::numeric_limits::min(); constexpr auto max_int = std::numeric_limits::max(); std::shared_ptr impl; @@ -284,10 +283,14 @@ VersionConstraints VersionConstraints::parse(QString const& value, Version::ParseMode mode) { std::vector constraints; - for (const auto& part : value.split(",")) { - constraints.push_back(VersionConstraint::parse(part.trimmed(), mode)); + auto parts = value.split(","); + for (auto& part : parts) { + // replace the part in-place to create a proper representation + part = part.simplified().replace(" ", ""); + + constraints.push_back(VersionConstraint::parse(part, mode)); } - return VersionConstraints(std::move(constraints)); + return VersionConstraints(parts.join(", "), std::move(constraints)); } bool VersionConstraints::matches(Version const& version) const @@ -298,8 +301,9 @@ bool VersionConstraints::matches(Version const& version) const }); } -VersionConstraints::VersionConstraints(std::vector checkers) - : m_Constraints{std::move(checkers)} +VersionConstraints::VersionConstraints(QString const& repr, + std::vector checkers) + : m_Repr{repr}, m_Constraints{std::move(checkers)} {} } // namespace MOBase diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ff9e192..b9845270 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(uibase-tests test_ifiletree.cpp test_strings.cpp test_versioning.cpp + test_extensions.cpp ) mo2_configure_tests(uibase-tests NO_SOURCES NO_MAIN NO_MOCK WARNINGS 4) target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/tests/test_extensions.cpp b/tests/test_extensions.cpp new file mode 100644 index 00000000..4e767282 --- /dev/null +++ b/tests/test_extensions.cpp @@ -0,0 +1,54 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include + +#include + +#include + +using namespace MOBase; + +class TestMetaData : public ExtensionMetaData +{ +public: + TestMetaData(std::filesystem::path const& path, QByteArray const& metadata) + : ExtensionMetaData(path, QJsonDocument::fromJson(metadata).object()) + {} +}; + +TEST(ExtensionsTest, MetaData) +{ + const auto metadata = TestMetaData({}, R"({ + "id": "mo2-game-bethesda", + "name": "Elder Scrolls & Fallout Games", + "version": "1.0.0", + "description": "ModOrganizer2 support for The Elder Scrolls & Fallout games.", + "author": { + "name": "Mod Organizer 2", + "homepage": "https://www.modorganizer.org/" + }, + "icon": "./tests/icon.png", + "contributors": [ + "AL", + "AnyOldName3", + "Holt59", + "Silarn" + ], + "type": "game", + "content": { + "plugins": { + "autodetect": true + }, + "translations": { + "autodetect": "translations" + } + } +})"); + + EXPECT_EQ("mo2-game-bethesda", metadata.identifier()); + EXPECT_EQ("Elder Scrolls & Fallout Games", metadata.name()); + EXPECT_FALSE(metadata.icon().isNull()); +} From e2216d9a9bc8ab8224f58d009d84c8a273ada7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Fri, 9 Aug 2024 13:56:12 +0200 Subject: [PATCH 24/28] Improve tests for extension metadata. --- include/uibase/extensions/extension.h | 5 ++ src/extension.cpp | 32 ++++++----- tests/CMakeLists.txt | 2 + .../extensions/mo2-example-extension/icon.png | Bin 0 -> 26498 bytes .../mo2-example-extension/metadata.json | 22 ++++++++ tests/test_extensions.cpp | 52 +++++------------- tests/test_main.cpp | 11 ++-- tests/test_strings.cpp | 7 +++ tests/test_utils.cpp | 31 +++++++++++ tests/test_utils.h | 27 +++++++++ tests/translations/extract_translations.py | 46 ++++++++++++++++ tests/translations/tests_en.qm | Bin 141 -> 228 bytes tests/translations/tests_en.ts | 15 ++++- tests/translations/tests_fr.qm | Bin 147 -> 416 bytes tests/translations/tests_fr.ts | 15 ++++- 15 files changed, 207 insertions(+), 58 deletions(-) create mode 100644 tests/data/extensions/mo2-example-extension/icon.png create mode 100644 tests/data/extensions/mo2-example-extension/metadata.json create mode 100644 tests/test_utils.cpp create mode 100644 tests/test_utils.h create mode 100644 tests/translations/extract_translations.py diff --git a/include/uibase/extensions/extension.h b/include/uibase/extensions/extension.h index c26f2bad..60b145e6 100644 --- a/include/uibase/extensions/extension.h +++ b/include/uibase/extensions/extension.h @@ -159,6 +159,11 @@ class QDLLEXPORT IExtension class QDLLEXPORT ExtensionFactory { public: + // load metadata from the given file, throws InvalidExtensionMetaDataException if the + // file does not exist or is invalid + // + static ExtensionMetaData loadMetaData(std::filesystem::path const& path); + // load an extension from the given directory, return a null-pointer if the extension // could not be load // diff --git a/src/extension.cpp b/src/extension.cpp index 761db63a..bc50efdc 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -101,7 +101,7 @@ ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, // TODO: name of the key // translation context - m_TranslationContext = jsonData["translationContext"].toString(""); + m_TranslationContext = jsonData["translation-context"].toString(""); if (jsonData.contains("icon")) { const QFileInfo icon{QDir(path), jsonData["icon"].toString()}; @@ -178,23 +178,21 @@ IExtension::IExtension(std::filesystem::path const& path, ExtensionMetaData&& me : m_Path{path}, m_MetaData{std::move(metadata)} {} -std::unique_ptr -ExtensionFactory::loadExtension(std::filesystem::path const& directory) +ExtensionMetaData ExtensionFactory::loadMetaData(std::filesystem::path const& path) { - const auto metadataPath = directory / METADATA_FILENAME; - - if (!exists(metadataPath)) { - log::warn("missing extension metadata in '{}'", directory.native()); - return nullptr; + if (!exists(path)) { + throw InvalidExtensionMetaDataException( + std::format("metadata file '{}' not found", path)); } // load the meta data QJsonParseError jsonError; QJsonDocument jsonMetaData; { - QFile file(metadataPath); + QFile file(path); if (!file.open(QFile::ReadOnly)) { - return {}; + throw InvalidExtensionMetaDataException( + std::format("failed to open metadata file '{}'", path)); } const auto jsonContent = file.readAll(); @@ -202,14 +200,18 @@ ExtensionFactory::loadExtension(std::filesystem::path const& directory) } if (jsonMetaData.isNull()) { - log::warn("failed to read metadata from '{}': {}", metadataPath.native(), - jsonError.errorString()); - return nullptr; + throw InvalidExtensionMetaDataException( + std::format("invalid metadata file '{}': {}", path, jsonError.errorString())); } + return ExtensionMetaData(path.parent_path(), jsonMetaData.object()); +} + +std::unique_ptr +ExtensionFactory::loadExtension(std::filesystem::path const& directory) +{ try { - return loadExtension(directory, - ExtensionMetaData(directory, jsonMetaData.object())); + return loadExtension(directory, loadMetaData(directory / METADATA_FILENAME)); } catch (InvalidExtensionMetaDataException const& ex) { log::warn("failed to load extension from '{}': invalid metadata ({})", directory.native(), ex.what()); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b9845270..08390a87 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.16) add_executable(uibase-tests EXCLUDE_FROM_ALL) target_sources(uibase-tests PRIVATE + test_utils.h + test_utils.cpp test_main.cpp test_formatters.cpp test_ifiletree.cpp diff --git a/tests/data/extensions/mo2-example-extension/icon.png b/tests/data/extensions/mo2-example-extension/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2b18c97bb2e77d2965cc8d3707a2a5b5d2bc8b01 GIT binary patch literal 26498 zcmeI4X-HI27>2*eG#9kUvdzTEtf0~%ZJn0MDYdjkB#pwE(L`*)0i!Z2r+&Dh77=Be z+Mf)x30ufEn-)wljPOTk8Hu7)8L*0WtY8JVCCR}+;TbFu2} zrE@1k$d+Uxj~VipA~eFMizs12s74*RKc=IxY`43f(wmm9yD{#F!;CM+C(a=OnI{6H zNB9<9EQ(Y3K1|%0eWS?wt|i3fk#F{=LtgD)8>_v{ecu`ir1$>EjsvanJ<3z^KP?|i z++KK+`F`0J4|XoSUVE}fv|F-dwb)VRUZ>#g<1;VW@wWGduu;6dvLN4$w~Z?0RNkhP z*d?O<85gi=0f|5nLji#%(gRWjmjc-ohQnBZW%8I9x@Dd_-S{+`mpAVhX{0)DpXFnH z{kg=P;=A9Km9MXNcGp;9L=j_kk?NMr&+E_Oy^MK50i(!})Ekd7ihT&kjRGJP<039m zMhPiUMsX>SDv&A|3n-&_Oboq@!srjz4is@EKoQ2T7z-%8cuZiZ8?yn71x%QTn1E$+ zDPWnfOvVB%lg9)s6PC$XfMtr9fMs$iV41_(GWmKXOYOAfHFi{*mAsayJhgVVB6_?$ z_vSe>Ba$eswPP&NfM455D$Rb7Vo%2AW~00yOgh}ce$K=FJ{as-ST=|CGkCLAO1kj9 zT2=2MdDPUbwT*l2^e&P=1)U!jGBL5}cO`AVz|4b9h@ncfqW*T=lyv<>I@&c5v^3Yj zd+h5>>ps`0F;y8vutu&b!5X;~u*P9+jaW!)3k1Xg;((!mQ5EhD7*)Zj3S$AKqlgKV zj$8^@CM=V&0L$bt0n3DCG8SN&A|_y&TnbnwER(SS%j7Wu%YS#5aIg79~Ac*tO95r$1+xK%^tm z83~AVTSO41BC*{IV*wB0ikLugETq6v7%l}?3*jML#=_rzn~3GzyKHk17H9QfB`T72EYIq00UqE41fVJ00zJS m7ytwREdzr${!hJJu6Jv9*_7Y7W84+_zCu^8*O*r6j{X3~U0Qem literal 0 HcmV?d00001 diff --git a/tests/data/extensions/mo2-example-extension/metadata.json b/tests/data/extensions/mo2-example-extension/metadata.json new file mode 100644 index 00000000..cccb7948 --- /dev/null +++ b/tests/data/extensions/mo2-example-extension/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "mo2-example-extension", + "name": "Example Extension for UI Base Tests", + "version": "1.0.0", + "description": "ModOrganizer2 example extension for UI Base Tests.", + "author": { + "name": "Mod Organizer 2", + "homepage": "https://www.modorganizer.org/" + }, + "icon": "icon.png", + "contributors": ["AL", "AnyOldName3", "Holt59", "Silarn"], + "type": "plugin", + "translation-context": "mo2-example-extension", + "content": { + "plugins": { + "autodetect": true + }, + "translations": { + "autodetect": "translations" + } + } +} diff --git a/tests/test_extensions.cpp b/tests/test_extensions.cpp index 4e767282..322fbf34 100644 --- a/tests/test_extensions.cpp +++ b/tests/test_extensions.cpp @@ -9,46 +9,24 @@ #include -using namespace MOBase; +#include "test_utils.h" -class TestMetaData : public ExtensionMetaData -{ -public: - TestMetaData(std::filesystem::path const& path, QByteArray const& metadata) - : ExtensionMetaData(path, QJsonDocument::fromJson(metadata).object()) - {} -}; +using namespace MOBase; TEST(ExtensionsTest, MetaData) { - const auto metadata = TestMetaData({}, R"({ - "id": "mo2-game-bethesda", - "name": "Elder Scrolls & Fallout Games", - "version": "1.0.0", - "description": "ModOrganizer2 support for The Elder Scrolls & Fallout games.", - "author": { - "name": "Mod Organizer 2", - "homepage": "https://www.modorganizer.org/" - }, - "icon": "./tests/icon.png", - "contributors": [ - "AL", - "AnyOldName3", - "Holt59", - "Silarn" - ], - "type": "game", - "content": { - "plugins": { - "autodetect": true - }, - "translations": { - "autodetect": "translations" - } - } -})"); - - EXPECT_EQ("mo2-game-bethesda", metadata.identifier()); - EXPECT_EQ("Elder Scrolls & Fallout Games", metadata.name()); + mo2::tests::TranslationHelper tr; + + const auto metadata = ExtensionFactory::loadMetaData( + "./tests/data/extensions/mo2-example-extension/metadata.json"); + + tr.switchLanguage("en"); + EXPECT_EQ("mo2-example-extension", metadata.identifier()); + EXPECT_EQ("Example Extension for UI Base Tests", metadata.name()); + + tr.switchLanguage("fr"); + EXPECT_EQ("mo2-example-extension", metadata.identifier()); + EXPECT_EQ("Extension Démo pour les tests UI Base", metadata.name()); + EXPECT_FALSE(metadata.icon().isNull()); } diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 0399b433..ff015903 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -3,13 +3,16 @@ #include #include +#include + int main(int argc, char** argv) { QCoreApplication app(argc, argv); - QTranslator translator; - if (translator.load("tests_fr", "tests/translations")) { - app.installTranslator(&translator); - } + + MOBase::log::createDefault({.name = "./mo2-tests.logs", + .maxLevel = MOBase::log::Levels::Info, + .pattern = ""}); + testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/tests/test_strings.cpp b/tests/test_strings.cpp index 56e75c9b..841ebb0c 100644 --- a/tests/test_strings.cpp +++ b/tests/test_strings.cpp @@ -9,6 +9,8 @@ #include +#include "test_utils.h" + using namespace MOBase; TEST(StringsTest, IEquals) @@ -37,6 +39,11 @@ TEST(StringsTest, IReplaceAll) // this is more a tests of the tests TEST(StringsTest, Translation) { + mo2::tests::TranslationHelper tr; + ASSERT_EQ("Translate to French", + QCoreApplication::translate("uibase-tests", "Translate to French")); + + tr.switchLanguage("fr"); ASSERT_EQ("Traduction en Français", QCoreApplication::translate("uibase-tests", "Translate to French")); } diff --git a/tests/test_utils.cpp b/tests/test_utils.cpp new file mode 100644 index 00000000..19a897ae --- /dev/null +++ b/tests/test_utils.cpp @@ -0,0 +1,31 @@ +#include "test_utils.h" + +#include + +namespace mo2::tests +{ +TranslationHelper::TranslationHelper() {} + +TranslationHelper::~TranslationHelper() +{ + release(); +} + +void TranslationHelper::release() +{ + if (m_Translator) { + QCoreApplication::removeTranslator(m_Translator.get()); + m_Translator.reset(); + } +} + +void TranslationHelper::switchLanguage(const QString& lang) +{ + m_Translator = std::make_unique(); + if (m_Translator->load("tests_" + lang, "tests/translations")) { + QCoreApplication::installTranslator(m_Translator.get()); + } else { + m_Translator.reset(); + } +} +} // namespace mo2::tests diff --git a/tests/test_utils.h b/tests/test_utils.h new file mode 100644 index 00000000..9362be50 --- /dev/null +++ b/tests/test_utils.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include + +namespace mo2::tests +{ + +class TranslationHelper +{ +public: + // create a new translation helper that can be used to switch language during tests + TranslationHelper(); + ~TranslationHelper(); + + // switch to the given language (should be available) + void switchLanguage(const QString& lang); + +private: + std::unique_ptr m_Translator; + + void release(); +}; + +} // namespace mo2::tests diff --git a/tests/translations/extract_translations.py b/tests/translations/extract_translations.py new file mode 100644 index 00000000..745749a4 --- /dev/null +++ b/tests/translations/extract_translations.py @@ -0,0 +1,46 @@ +import json +from pathlib import Path + +from PyQt6.lupdate.source_file import SourceFile +from PyQt6.lupdate.translation_file import TranslationFile +from PyQt6.lupdate.translations import Context, Message + +folder = Path(__file__).parent + +tr_files = [ + TranslationFile(path, no_obsolete=False, no_summary=False, verbose=True) + for path in folder.glob("*.ts") +] + +for metadata_path in folder.parent.joinpath("data", "extensions").glob("**/*.json"): + with open(metadata_path, "rb") as fp: + metadata = json.load(fp) + + if "translation-context" not in metadata: + continue + + source = SourceFile(filename=metadata_path) + + context = Context(name=metadata["translation-context"]) + + for key in ("name", "description"): + if key not in metadata: + continue + + context.messages.append( + Message( + filename=metadata_path, + line_nr=-1, + source=metadata[key], + comment=None, + numerus=None, + ) + ) + + source.contexts.append(context) + + for tr_file in tr_files: + tr_file.update(source) + +for tr_file in tr_files: + tr_file.write() diff --git a/tests/translations/tests_en.qm b/tests/translations/tests_en.qm index d5ca50195d8899565e374e2405b25297d85c8ced..837385dd47bd54d5df1a9cb58a454032e56e1b1d 100644 GIT binary patch literal 228 zcmcE7ks@*G{hX<16=n7(EZlo{IRgU&YieG6Xs{EICBU33)eNK=*h(Mg0V&^1AhDA9 zKM-($g@J^!YeiyiK~AcIYeh+FUU6oAoUB~szOL=aY->dP@QOQzL9P! rL@khps%1nq%*Z!C#lI*$F)yTsvJn;G+Gaj*&RS@nHU)X=m1DB delta 118 zcmaFD*vmLUE}w(_`_dl_K#&QjUj}gh#`?7kD-_$2S}GNq%tS~$$TK`29!w! z$|N&naDX(i0kLpMQDR + + mo2-example-extension + + + Example Extension for UI Base Tests + + + + + ModOrganizer2 example extension for UI Base Tests. + + + uibase-tests Translate to French - Translate to French + Translate to French diff --git a/tests/translations/tests_fr.qm b/tests/translations/tests_fr.qm index 80958d6e11a850ede8d6266c2a39254a0abce8ba..fc1ef2fa025b1ef4296ff4fe04aeaceb897b7b63 100644 GIT binary patch literal 416 zcmb7g41rcoK_?D0DgJTsW8iJI7Di&g?c4s|%hkj4`y+sjiMu>KkC}R5v3pI& z=6q>rcR3Jf$aIDTH7Y0!JaSmN-~=Bim<1(u4#zZO$J3jxr#5+v0X0U-mu|HOCQye(SH8>Mc44C<{qE9N-Jrp8EtV3ZY!3@E9P;pQ)FA;g ujQC?p48sWv_8cODtpShz4}LVX*~m>(lP}aovI3<4Ii~;5nj5a^guxe1>tk#H delta 124 zcmZ3$JehHVTqy_p_oY7=fFKiy6PSU74nqh-5kn$F3PUMFGD8VaBA+3TL4hF^OuB*P z@)({2Rb(<0bAYt60kLpMQDR + + mo2-example-extension + + + Example Extension for UI Base Tests + Extension Démo pour les tests UI Base + + + + ModOrganizer2 example extension for UI Base Tests. + Exemple d'extension ModOrganizer2 pour les tests UI Base. + + uibase-tests Translate to French - Traduction en Français + Traduction en Français From 3f5e0938efdd42fcd247ca6571acc43e54867a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Fri, 9 Aug 2024 14:22:51 +0200 Subject: [PATCH 25/28] Fix translations for tests. --- tests/translations/extract_translations.py | 25 ++++++++++++++++++++- tests/translations/tests_en.qm | Bin 228 -> 326 bytes tests/translations/tests_en.ts | 3 ++- tests/translations/tests_fr.qm | Bin 416 -> 520 bytes tests/translations/tests_fr.ts | 5 +++-- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/tests/translations/extract_translations.py b/tests/translations/extract_translations.py index 745749a4..016f6b42 100644 --- a/tests/translations/extract_translations.py +++ b/tests/translations/extract_translations.py @@ -12,6 +12,25 @@ for path in folder.glob("*.ts") ] +sources: list[SourceFile] = [] + +# add custom source used in test +source = SourceFile(filename="uibase-tests") +context = Context(name="uibase-tests") +context.messages = [ + Message( + filename=__file__, + line_nr=-1, + source="Translate to French", + comment=None, + numerus=None, + ) +] +source.contexts.append(context) +sources.append(source) + + +# add metadata for tests extension for metadata_path in folder.parent.joinpath("data", "extensions").glob("**/*.json"): with open(metadata_path, "rb") as fp: metadata = json.load(fp) @@ -39,8 +58,12 @@ source.contexts.append(context) - for tr_file in tr_files: + sources.append(source) + +for tr_file in tr_files: + for source in sources: tr_file.update(source) + for tr_file in tr_files: tr_file.write() diff --git a/tests/translations/tests_en.qm b/tests/translations/tests_en.qm index 837385dd47bd54d5df1a9cb58a454032e56e1b1d..472bdf10c685910a35bc2abc60c202b7caa4d190 100644 GIT binary patch delta 138 zcmaFDc#LU+oSX!6u2eGv5U`a#&I3}u?BAFE0MaWn85kIuCz`BGWCrro7(y6|7!n!s z7>XHkfOH8%DuV)$%mr=OmV-DwO0axD}=5C1 uibase-tests + Translate to French - Translate to French + Translate to French diff --git a/tests/translations/tests_fr.qm b/tests/translations/tests_fr.qm index fc1ef2fa025b1ef4296ff4fe04aeaceb897b7b63..101f64b4270ef0f00739145e2732a2e8f1c9063e 100644 GIT binary patch delta 145 zcmZ3$+`%$IPELY3SE`u-2-r#==K-nd?BAFEU|?WO%>)uhH=3j}rZWSDbr?bziWm|Z zQW#1Zk{L=EG8yt2@)#5tQo*DfST2v@IZ#C=Loo-~JRl()Qk0lioRe6Rs!)=z;8v8H emz=>46z3_;OiC ModOrganizer2 example extension for UI Base Tests. - Exemple d'extension ModOrganizer2 pour les tests UI Base. + Exemple d'extension ModOrganizer2 pour les tests UI Base. uibase-tests + Translate to French - Traduction en Français + Traduction en Français From 5c1d8c52a57200a80e8a3156eba777d754ab8366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 10 Aug 2024 10:40:20 +0200 Subject: [PATCH 26/28] Start refactoring setting system for plugins/extensions. --- include/uibase/extensions/extensionsetting.h | 89 ++++++++++++++++++++ include/uibase/imodinterface.h | 22 ++--- include/uibase/iplugin.h | 21 +++-- include/uibase/pluginsetting.h | 22 +---- src/CMakeLists.txt | 1 + src/extension.cpp | 6 +- 6 files changed, 122 insertions(+), 39 deletions(-) create mode 100644 include/uibase/extensions/extensionsetting.h diff --git a/include/uibase/extensions/extensionsetting.h b/include/uibase/extensions/extensionsetting.h new file mode 100644 index 00000000..cd1ee7f6 --- /dev/null +++ b/include/uibase/extensions/extensionsetting.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + +namespace MOBase +{ + +// class representing a group of settings +// +class SettingGroup +{ +public: + SettingGroup(QString const& name, QString const& title, QString const& description) + : m_Name{name}, m_Title{title}, m_Description{description} + {} + + // return the (internal) name of this group, localization independent + // + const auto& name() const { return m_Name; } + + // retrieve the title of this group, can be localized + // + const auto& title() const { return m_Title; } + + // retrieve the description of this group, can be localized + // + const auto& description() const { return m_Description; } + +private: + QString m_Name, m_Title, m_Description; +}; + +// class that represents an extension or a plugin setting +// +class Setting +{ +public: + // deprecated constructor that was previously available as PluginSettin + // + [[deprecated]] Setting(const QString& name, const QString& description, + const QVariant& defaultValue) + : m_Name{name}, m_Title{name}, m_Description{description}, m_Group{}, + m_DefaultValue{defaultValue} + {} + + Setting(const QString& name, const QString& title, const QString& description, + const QVariant& defaultValue) + : m_Name{name}, m_Title{title}, m_Description{description}, m_Group{}, + m_DefaultValue{defaultValue} + {} + + Setting(const QString& name, const QString& title, const QString& description, + const QString& group, const QVariant& defaultValue) + : m_Name{name}, m_Title{title}, m_Description{description}, m_Group{group}, + m_DefaultValue{defaultValue} + {} + +public: + // return the (internal) name of this setting, localization independent + // + const auto& name() const { return m_Name; } + + // retrieve the title of this setting, can be localized + // + const auto& title() const { return m_Title; } + + // retrieve the description of this setting, can be localized + // + const auto& description() const { return m_Description; } + + // retrieve the name of the group this settings belongs to or an empty string if there + // is none + // + const auto& group() const { return m_Group; } + + // retrieve the default value of this setting + // + const auto& defaultValue() const { return m_DefaultValue; } + +private: + QString m_Name; + QString m_Title; + QString m_Description; + QString m_Group; + QVariant m_DefaultValue; +}; + +} // namespace MOBase diff --git a/include/uibase/imodinterface.h b/include/uibase/imodinterface.h index ca8c6ff6..b093aea5 100644 --- a/include/uibase/imodinterface.h +++ b/include/uibase/imodinterface.h @@ -269,17 +269,17 @@ class IModInterface virtual void setUrl(const QString& url) = 0; public: // Plugin operations: - /** - * @brief Retrieve the specified setting in this mod for a plugin. - * - * @param pluginName Name of the plugin for which to retrieve a setting. This should - * always be IPlugin::name() unless you have a really good reason to access - * settings of another plugin. - * @param key Identifier of the setting. - * @param defaultValue The default value to return if the setting does not exist. - * - * @return the setting, if found, or the default value. - */ + /** + * @brief Retrieve the specified setting in this mod for a plugin. + * + * @param pluginName Name of the plugin for which to retrieve a setting. This should + * always be IPlugin::name() unless you have a really good reason to access + * settings of another plugin. + * @param key Identifier of the setting. + * @param defaultValue The default value to return if the setting does not exist. + * + * @return the setting, if found, or the default value. + */ virtual QVariant pluginSetting(const QString& pluginName, const QString& key, const QVariant& defaultValue = QVariant()) const = 0; diff --git a/include/uibase/iplugin.h b/include/uibase/iplugin.h index c86d774b..5f8b0cb5 100644 --- a/include/uibase/iplugin.h +++ b/include/uibase/iplugin.h @@ -21,14 +21,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #ifndef IPLUGIN_H #define IPLUGIN_H -#include "imoinfo.h" -#include "pluginrequirements.h" -#include "pluginsetting.h" -#include "versioninfo.h" +#include + #include #include #include -#include + +#include "extensions/extensionsetting.h" +#include "imoinfo.h" +#include "pluginrequirements.h" + +// deprecated header +#include "pluginsetting.h" namespace MOBase { @@ -88,7 +92,12 @@ class IPlugin * @note Plugin can store "hidden" (from the user) settings using * IOrganizer::persistent / IOrganizer::setPersistent. */ - virtual QList settings() const = 0; + virtual QList settings() const = 0; + + /** + * @return the list of groups for settings. + */ + virtual QList settingGroups() const { return {}; } /** * @return whether the plugin should be enabled by default diff --git a/include/uibase/pluginsetting.h b/include/uibase/pluginsetting.h index 6844f4d0..0b760d55 100644 --- a/include/uibase/pluginsetting.h +++ b/include/uibase/pluginsetting.h @@ -21,29 +21,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #ifndef PLUGINSETTING_H #define PLUGINSETTING_H -#include -#include -#include +#include "extensions/extensionsetting.h" namespace MOBase { -/** - * @brief struct to hold the user-configurable parameters a plugin accepts. The purpose - * of this struct is only to inform the application what settings to offer to the user, - * it does not hold the actual value - */ -struct PluginSetting -{ - PluginSetting(const QString& key, const QString& description, - const QVariant& defaultValue) - : key(key), description(description), defaultValue(defaultValue) - {} - - QString key; - QString description; - QVariant defaultValue; -}; +// deprecated alias +using PluginSetting [[deprecated]] = Setting; } // namespace MOBase diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ebd77919..73d4b318 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,7 @@ set(root_headers ) set(extension_headers ../include/uibase/extensions/extension.h + ../include/uibase/extensions/extensionsetting.h ../include/uibase/extensions/iextensionlist.h ../include/uibase/extensions/ipluginloader.h ../include/uibase/extensions/requirements.h diff --git a/src/extension.cpp b/src/extension.cpp index bc50efdc..898beabb 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -96,10 +96,10 @@ ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, Version::ParseMode::SemVer); } catch (InvalidVersionException const& ex) { throw InvalidExtensionMetaDataException( - std::format("invalid or missing version '{}'", jsonData["version"].toString())); + std::format("invalid or missing version '{}': {}", + jsonData["version"].toString(), ex.what())); } - // TODO: name of the key // translation context m_TranslationContext = jsonData["translation-context"].toString(""); @@ -337,7 +337,7 @@ TranslationExtension::parseTranslation(std::filesystem::path const& extensionFol QLocale locale(identifier); name = QString("%1 (%2)") .arg(locale.nativeLanguageName()) - .arg(locale.nativeCountryName()); + .arg(locale.nativeTerritoryName()); } return std::make_shared(identifier.toStdString(), name.toStdString(), From 3a64b4c947d5e7f6afa6ad38277d32785a545cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 10 Aug 2024 15:35:54 +0200 Subject: [PATCH 27/28] Add possibility to specify homepage for authors/contributors. --- include/uibase/extensions/extension.h | 8 ++++++-- src/extension.cpp | 11 +++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/include/uibase/extensions/extension.h b/include/uibase/extensions/extension.h index 60b145e6..5bcc072c 100644 --- a/include/uibase/extensions/extension.h +++ b/include/uibase/extensions/extension.h @@ -36,18 +36,22 @@ enum class ExtensionType class QDLLEXPORT ExtensionContributor { public: - ExtensionContributor(QString name); + ExtensionContributor(QString name, QString homepage); // retrieve the name of the contributor // const auto& name() const { return m_Name; } + // retrieve the homagepage of the contributor + // + const auto& homepage() const { return m_Homepage; } + private: ExtensionContributor() = default; friend class ExtensionMetaData; - QString m_Name; + QString m_Name, m_Homepage; }; class QDLLEXPORT ExtensionMetaData diff --git a/src/extension.cpp b/src/extension.cpp index 898beabb..651cf13e 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -46,22 +46,25 @@ namespace ExtensionContributor parseContributor(QJsonValue const& value) { if (value.isNull()) { - return ExtensionContributor(""); + return ExtensionContributor("", ""); } // TODO: handle more fields in the future, handle string authors similar to NPM if (value.isObject()) { const auto contrib = value.toObject(); - return ExtensionContributor(contrib["name"].toString()); + return ExtensionContributor(contrib["name"].toString(), + contrib["homepage"].toString()); } - return ExtensionContributor(value.toString()); + return ExtensionContributor(value.toString(), {}); } } // namespace -ExtensionContributor::ExtensionContributor(QString name) : m_Name{name} {} +ExtensionContributor::ExtensionContributor(QString name, QString homepage) + : m_Name{std::move(name)}, m_Homepage{std::move(homepage)} +{} ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, QJsonObject const& jsonData) From 2d4ffb47e796572b0f5c1d059026816c732c951e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sat, 10 Aug 2024 15:36:02 +0200 Subject: [PATCH 28/28] Add back IPlugin::requirements. --- include/uibase/iplugin.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/uibase/iplugin.h b/include/uibase/iplugin.h index 5f8b0cb5..0e5a3802 100644 --- a/include/uibase/iplugin.h +++ b/include/uibase/iplugin.h @@ -85,6 +85,18 @@ class IPlugin */ virtual QString localizedName() const { return name(); } + /** + * @brief Retrieve the requirements for the plugins. + * + * This method is called right after init(). + * + * @return the requirements for this plugin. + */ + virtual std::vector> requirements() const + { + return {}; + } + /** * @return the list of configurable settings for this plugin (in the user interface). * The list may be empty.