diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27b73f02..ea051889 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,26 +2,78 @@ name: Build UIBase on: push: - branches: master + branches: [master] + tags: + - "*" pull_request: types: [opened, synchronize, reopened] +env: + VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite + jobs: build: runs-on: windows-2022 steps: - - name: Build UI Base - id: build-uibase + - name: Configure UIBase + id: configure-uibase uses: ModOrganizer2/build-with-mob-action@master with: - mo2-third-parties: gtest spdlog boost - mo2-dependencies: cmake_common - mo2-cmake-command: -DUIBASE_TESTS=1 .. + # skip build because we are going to build both Debug and RelWithDebInfo here + mo2-skip-build: true - - name: Build UI Base Tests - run: cmake --build vsbuild --config RelWithDebInfo -j4 --target uibase-tests - working-directory: ${{ steps.build-uibase.outputs.working-directory }} + # build both Debug and RelWithDebInfo for package + - name: Build UI Base + working-directory: ${{ steps.configure-uibase.outputs.working-directory }} + 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 }} + working-directory: ${{ steps.configure-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 + working-directory: ${{ steps.configure-uibase.outputs.working-directory }} + 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}\msvc2022_64;${{ github.workspace }}\install\lib\cmake\" + cmake --build build --config Debug + cmake --build build --config Release + cmake --build build --config RelWithDebInfo + working-directory: ${{ steps.configure-uibase.outputs.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..a55b4baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ edit +.vscode CMakeLists.txt.user /msbuild.log /*std*.log /*build /src/uibasetests_en.ts +/install +/tests/cmake/build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..3103a1f1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.5 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/CMakeLists.txt b/CMakeLists.txt index 800fa66d..065e6139 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(BUILD_TESTING ${BUILD_TESTING} CACHE BOOL "build tests for uibase") +if (BUILD_TESTING) enable_testing() add_subdirectory(tests) endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 00000000..62939333 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,73 @@ +{ + "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" + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_FEATURES": { + "type": "STRING", + "value": "standalone;testing" + } + }, + "inherits": "vs2022-windows", + "name": "vs2022-windows-standalone" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} diff --git a/README.md b/README.md index 97468b30..b9abeaae 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 ` + -DBUILD_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: + +- `BUILD_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 98% rename from src/exceptions.h rename to include/uibase/exceptions.h index 433b9be7..c89a4833 100644 --- a/src/exceptions.h +++ b/include/uibase/exceptions.h @@ -3,6 +3,7 @@ #include +#include #include #include "dllimport.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 78% rename from src/formatters.h rename to include/uibase/formatters.h index 78ee90fd..870e01d4 100644 --- a/src/formatters.h +++ b/include/uibase/formatters.h @@ -3,4 +3,4 @@ #include "./formatters/enums.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/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 99% rename from src/game_features/igamefeatures.h rename to include/uibase/game_features/igamefeatures.h index c625bfbf..2d89b3cc 100644 --- a/src/game_features/igamefeatures.h +++ b/include/uibase/game_features/igamefeatures.h @@ -163,4 +163,4 @@ class IGameFeatures } // namespace MOBase -#endif \ No newline at end of file +#endif 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 99% rename from src/ifiletree.h rename to include/uibase/ifiletree.h index bf06d1e3..83781b28 100644 --- a/src/ifiletree.h +++ b/include/uibase/ifiletree.h @@ -598,7 +598,7 @@ class QDLLEXPORT IFileTree : public virtual FileTreeEntry */ QString pathTo(std::shared_ptr entry, QString sep = "\\") const { - return entry->pathFrom(astree()); + return entry->pathFrom(astree(), sep); } public: // Walk operations 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 98% rename from src/imoinfo.h rename to include/uibase/imoinfo.h index 407a379a..bf6588d5 100644 --- a/src/imoinfo.h +++ b/include/uibase/imoinfo.h @@ -30,11 +30,12 @@ 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" #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/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/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/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..fa1d0a66 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/versioning.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 + registry.cpp + report.cpp + safewritefile.cpp + scopeguard.cpp + steamutility.cpp + strings.cpp + utility.cpp + versioning.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 ) -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/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/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/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/strings.cpp b/src/strings.cpp new file mode 100644 index 00000000..768f6921 --- /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(); + + std::size_t i = 0; + while (input.size() - i >= search_length) { + if (iequals(std::string_view(input).substr(i, search_length), search)) { + input.replace(i, search_length, replace); + i += replace_length; + } else { + ++i; + } + } +} + +} // namespace MOBase diff --git a/src/uibase_en.ts b/src/uibase_en.ts new file mode 100644 index 00000000..defc5452 --- /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/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 022cd8ca..7ff9e192 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,13 @@ 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_main.cpp + test_formatters.cpp + test_ifiletree.cpp + test_strings.cpp + test_versioning.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/cmake/CMakeLists.txt b/tests/cmake/CMakeLists.txt new file mode 100644 index 00000000..e1d636f7 --- /dev/null +++ b/tests/cmake/CMakeLists.txt @@ -0,0 +1,10 @@ +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) +set_target_properties(plugin PROPERTIES CXX_STANDARD 20) 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_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 new file mode 100644 index 00000000..0185f83a --- /dev/null +++ b/tests/test_strings.cpp @@ -0,0 +1,47 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include + +#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")); + + ASSERT_EQ( + "data path: C:/Users/USERNAME/AppData/Local/ModOrganizer/Starfield", + ireplace_all("data path: C:/Users/lords/AppData/Local/ModOrganizer/Starfield", + "/lords", "/USERNAME")); +} + +// 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/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)); +} diff --git a/tests/translations/tests_en.qm b/tests/translations/tests_en.qm new file mode 100644 index 00000000..d5ca5019 Binary files /dev/null and b/tests/translations/tests_en.qm differ diff --git a/tests/translations/tests_en.ts b/tests/translations/tests_en.ts new file mode 100644 index 00000000..1f54d866 --- /dev/null +++ b/tests/translations/tests_en.ts @@ -0,0 +1,11 @@ + + + + + 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 00000000..80958d6e Binary files /dev/null and b/tests/translations/tests_fr.qm differ 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.json b/vcpkg.json new file mode 100644 index 00000000..47779187 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,34 @@ +{ + "dependencies": ["spdlog"], + "overrides": [ + { + "name": "spdlog", + "version": "1.15.3" + } + ], + "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": "294f76666c3000630d828703e675814c05a4fd43" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673", + "packages": ["mo2-cmake", "spdlog"] + } + ] + } +}