aboutsummaryrefslogtreecommitdiff
path: root/libs/uibase
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/uibase
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/uibase')
-rw-r--r--libs/uibase/.clang-format41
-rw-r--r--libs/uibase/.git-blame-ignore-revs1
-rw-r--r--libs/uibase/.gitattributes7
-rw-r--r--libs/uibase/.github/workflows/build.yml79
-rw-r--r--libs/uibase/.github/workflows/linting.yml17
-rw-r--r--libs/uibase/.gitignore9
-rw-r--r--libs/uibase/.pre-commit-config.yaml20
-rw-r--r--libs/uibase/CMakeLists.txt34
-rw-r--r--libs/uibase/CMakePresets.json73
-rw-r--r--libs/uibase/README.md56
-rw-r--r--libs/uibase/cmake/config.cmake.in13
-rw-r--r--libs/uibase/include/uibase/delayedfilewriter.h74
-rw-r--r--libs/uibase/include/uibase/diagnosisreport.h51
-rw-r--r--libs/uibase/include/uibase/dllimport.h46
-rw-r--r--libs/uibase/include/uibase/errorcodes.h21
-rw-r--r--libs/uibase/include/uibase/eventfilter.h45
-rw-r--r--libs/uibase/include/uibase/exceptions.h58
-rw-r--r--libs/uibase/include/uibase/executableinfo.h67
-rw-r--r--libs/uibase/include/uibase/expanderwidget.h63
-rw-r--r--libs/uibase/include/uibase/filemapping.h17
-rw-r--r--libs/uibase/include/uibase/filesystemutilities.h36
-rw-r--r--libs/uibase/include/uibase/filterwidget.h148
-rw-r--r--libs/uibase/include/uibase/finddialog.h80
-rw-r--r--libs/uibase/include/uibase/formatters.h5
-rw-r--r--libs/uibase/include/uibase/formatters/enums.h16
-rw-r--r--libs/uibase/include/uibase/formatters/qt.h87
-rw-r--r--libs/uibase/include/uibase/formatters/strings.h89
-rw-r--r--libs/uibase/include/uibase/game_features/bsainvalidation.h26
-rw-r--r--libs/uibase/include/uibase/game_features/dataarchives.h35
-rw-r--r--libs/uibase/include/uibase/game_features/game_feature.h40
-rw-r--r--libs/uibase/include/uibase/game_features/gameplugins.h25
-rw-r--r--libs/uibase/include/uibase/game_features/igamefeatures.h166
-rw-r--r--libs/uibase/include/uibase/game_features/localsavegames.h21
-rw-r--r--libs/uibase/include/uibase/game_features/moddatachecker.h66
-rw-r--r--libs/uibase/include/uibase/game_features/moddatacontent.h122
-rw-r--r--libs/uibase/include/uibase/game_features/savegameinfo.h52
-rw-r--r--libs/uibase/include/uibase/game_features/scriptextender.h53
-rw-r--r--libs/uibase/include/uibase/game_features/unmanagedmods.h45
-rw-r--r--libs/uibase/include/uibase/guessedvalue.h252
-rw-r--r--libs/uibase/include/uibase/idownloadmanager.h102
-rw-r--r--libs/uibase/include/uibase/iexecutable.h84
-rw-r--r--libs/uibase/include/uibase/iexecutableslist.h75
-rw-r--r--libs/uibase/include/uibase/ifiletree.h1130
-rw-r--r--libs/uibase/include/uibase/ifiletree_utils.h82
-rw-r--r--libs/uibase/include/uibase/iinstallationmanager.h128
-rw-r--r--libs/uibase/include/uibase/iinstance.h61
-rw-r--r--libs/uibase/include/uibase/iinstancemanager.h66
-rw-r--r--libs/uibase/include/uibase/imodinterface.h343
-rw-r--r--libs/uibase/include/uibase/imodlist.h229
-rw-r--r--libs/uibase/include/uibase/imodrepositorybridge.h219
-rw-r--r--libs/uibase/include/uibase/imoinfo.h616
-rw-r--r--libs/uibase/include/uibase/iplugin.h146
-rw-r--r--libs/uibase/include/uibase/iplugindiagnose.h103
-rw-r--r--libs/uibase/include/uibase/ipluginfilemapper.h49
-rw-r--r--libs/uibase/include/uibase/iplugingame.h380
-rw-r--r--libs/uibase/include/uibase/iplugingamefeatures.h61
-rw-r--r--libs/uibase/include/uibase/iplugininstaller.h157
-rw-r--r--libs/uibase/include/uibase/iplugininstallercustom.h79
-rw-r--r--libs/uibase/include/uibase/iplugininstallersimple.h69
-rw-r--r--libs/uibase/include/uibase/ipluginlist.h268
-rw-r--r--libs/uibase/include/uibase/ipluginmodpage.h92
-rw-r--r--libs/uibase/include/uibase/ipluginpreview.h72
-rw-r--r--libs/uibase/include/uibase/ipluginproxy.h83
-rw-r--r--libs/uibase/include/uibase/iplugintool.h87
-rw-r--r--libs/uibase/include/uibase/iprofile.h41
-rw-r--r--libs/uibase/include/uibase/isavegame.h58
-rw-r--r--libs/uibase/include/uibase/isavegameinfowidget.h30
-rw-r--r--libs/uibase/include/uibase/json.h95
-rw-r--r--libs/uibase/include/uibase/lineeditclear.h40
-rw-r--r--libs/uibase/include/uibase/linklabel.h38
-rw-r--r--libs/uibase/include/uibase/log.h346
-rw-r--r--libs/uibase/include/uibase/memoizedlock.h61
-rw-r--r--libs/uibase/include/uibase/moassert.h36
-rw-r--r--libs/uibase/include/uibase/modrepositoryfileinfo.h56
-rw-r--r--libs/uibase/include/uibase/nxmurl.h87
-rw-r--r--libs/uibase/include/uibase/pluginrequirements.h193
-rw-r--r--libs/uibase/include/uibase/pluginsetting.h50
-rw-r--r--libs/uibase/include/uibase/questionboxmemory.h112
-rw-r--r--libs/uibase/include/uibase/registry.h39
-rw-r--r--libs/uibase/include/uibase/report.h117
-rw-r--r--libs/uibase/include/uibase/safewritefile.h72
-rw-r--r--libs/uibase/include/uibase/scopeguard.h296
-rw-r--r--libs/uibase/include/uibase/sortabletreewidget.h64
-rw-r--r--libs/uibase/include/uibase/steamutility.h47
-rw-r--r--libs/uibase/include/uibase/strings.h22
-rw-r--r--libs/uibase/include/uibase/taskprogressmanager.h60
-rw-r--r--libs/uibase/include/uibase/textviewer.h97
-rw-r--r--libs/uibase/include/uibase/tutorabledialog.h65
-rw-r--r--libs/uibase/include/uibase/tutorialcontrol.h83
-rw-r--r--libs/uibase/include/uibase/tutorialmanager.h109
-rw-r--r--libs/uibase/include/uibase/utility.h443
-rw-r--r--libs/uibase/include/uibase/versioninfo.h185
-rw-r--r--libs/uibase/include/uibase/versioning.h181
-rw-r--r--libs/uibase/include/uibase/widgetutility.h22
l---------libs/uibase/include/uibase/windows_compat.h1
-rw-r--r--libs/uibase/src/CMakeLists.txt201
-rw-r--r--libs/uibase/src/delayedfilewriter.cpp49
-rw-r--r--libs/uibase/src/diagnosisreport.cpp25
-rw-r--r--libs/uibase/src/errorcodes.cpp2361
-rw-r--r--libs/uibase/src/eventfilter.cpp34
-rw-r--r--libs/uibase/src/executableinfo.cpp105
-rw-r--r--libs/uibase/src/expanderwidget.cpp93
-rw-r--r--libs/uibase/src/filesystemutilities.cpp68
-rw-r--r--libs/uibase/src/filterwidget.cpp693
-rw-r--r--libs/uibase/src/finddialog.cpp51
-rw-r--r--libs/uibase/src/finddialog.ui76
-rw-r--r--libs/uibase/src/guessedvalue.cpp1
-rw-r--r--libs/uibase/src/ifiletree.cpp1029
-rw-r--r--libs/uibase/src/imodrepositorybridge.cpp1
-rw-r--r--libs/uibase/src/imoinfo.cpp23
-rw-r--r--libs/uibase/src/json.cpp557
-rw-r--r--libs/uibase/src/lineeditclear.cpp50
-rw-r--r--libs/uibase/src/linklabel.cpp27
-rw-r--r--libs/uibase/src/log.cpp411
-rw-r--r--libs/uibase/src/modrepositoryfileinfo.cpp81
-rw-r--r--libs/uibase/src/nxmurl.cpp44
-rw-r--r--libs/uibase/src/pch.cpp1
-rw-r--r--libs/uibase/src/pch.h139
-rw-r--r--libs/uibase/src/pluginrequirements.cpp138
-rw-r--r--libs/uibase/src/questionboxmemory.cpp200
-rw-r--r--libs/uibase/src/questionboxmemory.ui171
-rw-r--r--libs/uibase/src/registry.cpp189
-rw-r--r--libs/uibase/src/report.cpp453
-rw-r--r--libs/uibase/src/safewritefile.cpp84
-rw-r--r--libs/uibase/src/scopeguard.cpp6
-rw-r--r--libs/uibase/src/sortabletreewidget.cpp116
-rw-r--r--libs/uibase/src/steamutility.cpp108
-rw-r--r--libs/uibase/src/strings.cpp46
-rw-r--r--libs/uibase/src/taskdialog.ui359
-rw-r--r--libs/uibase/src/taskprogressmanager.cpp125
-rw-r--r--libs/uibase/src/textviewer.cpp278
-rw-r--r--libs/uibase/src/textviewer.ui55
-rw-r--r--libs/uibase/src/tutorabledialog.cpp40
-rw-r--r--libs/uibase/src/tutorialcontrol.cpp384
-rw-r--r--libs/uibase/src/tutorialmanager.cpp112
-rw-r--r--libs/uibase/src/uibase_en.ts407
-rw-r--r--libs/uibase/src/utility.cpp1260
-rw-r--r--libs/uibase/src/version.rc37
-rw-r--r--libs/uibase/src/versioninfo.cpp432
-rw-r--r--libs/uibase/src/versioning.cpp278
-rw-r--r--libs/uibase/src/widgetutility.cpp59
-rw-r--r--libs/uibase/tests/CMakeLists.txt13
-rw-r--r--libs/uibase/tests/cmake/CMakeLists.txt10
-rw-r--r--libs/uibase/tests/cmake/plugin.cpp4
-rw-r--r--libs/uibase/tests/test_formatters.cpp48
-rw-r--r--libs/uibase/tests/test_ifiletree.cpp1172
-rw-r--r--libs/uibase/tests/test_main.cpp15
-rw-r--r--libs/uibase/tests/test_strings.cpp47
-rw-r--r--libs/uibase/tests/test_versioning.cpp90
-rw-r--r--libs/uibase/tests/translations/tests_en.qmbin0 -> 141 bytes
-rw-r--r--libs/uibase/tests/translations/tests_en.ts11
-rw-r--r--libs/uibase/tests/translations/tests_fr.qmbin0 -> 147 bytes
-rw-r--r--libs/uibase/tests/translations/tests_fr.ts11
-rw-r--r--libs/uibase/vcpkg.json34
154 files changed, 22995 insertions, 0 deletions
diff --git a/libs/uibase/.clang-format b/libs/uibase/.clang-format
new file mode 100644
index 0000000..6098e1f
--- /dev/null
+++ b/libs/uibase/.clang-format
@@ -0,0 +1,41 @@
+---
+# We'll use defaults from the LLVM style, but with 4 columns indentation.
+BasedOnStyle: LLVM
+IndentWidth: 2
+---
+Language: Cpp
+DeriveLineEnding: false
+UseCRLF: true
+DerivePointerAlignment: false
+PointerAlignment: Left
+AlignConsecutiveAssignments: true
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: Empty
+AlwaysBreakTemplateDeclarations: Yes
+AccessModifierOffset: -2
+AlignTrailingComments: true
+SpacesBeforeTrailingComments: 2
+NamespaceIndentation: Inner
+MaxEmptyLinesToKeep: 1
+BreakBeforeBraces: Custom
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: true
+ AfterControlStatement: false
+ AfterEnum: true
+ AfterFunction: true
+ AfterNamespace: true
+ AfterStruct: true
+ AfterUnion: true
+ AfterExternBlock: true
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+ SplitEmptyNamespace: true
+ColumnLimit: 88
+ForEachMacros: ['Q_FOREACH', 'foreach']
diff --git a/libs/uibase/.git-blame-ignore-revs b/libs/uibase/.git-blame-ignore-revs
new file mode 100644
index 0000000..712711a
--- /dev/null
+++ b/libs/uibase/.git-blame-ignore-revs
@@ -0,0 +1 @@
+218607a6ce4d41112bd3995ba319255aec3c5c2e
diff --git a/libs/uibase/.gitattributes b/libs/uibase/.gitattributes
new file mode 100644
index 0000000..f869712
--- /dev/null
+++ b/libs/uibase/.gitattributes
@@ -0,0 +1,7 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Explicitly declare text files you want to always be normalized and converted
+# to native line endings on checkout.
+*.cpp text eol=crlf
+*.h text eol=crlf
diff --git a/libs/uibase/.github/workflows/build.yml b/libs/uibase/.github/workflows/build.yml
new file mode 100644
index 0000000..8d6fbd5
--- /dev/null
+++ b/libs/uibase/.github/workflows/build.yml
@@ -0,0 +1,79 @@
+name: Build UIBase
+
+on:
+ push:
+ branches: [master]
+ tags:
+ - "*"
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+env:
+ VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }}
+
+jobs:
+ build:
+ runs-on: windows-2022
+ steps:
+ - name: Configure UIBase
+ id: configure-uibase
+ uses: ModOrganizer2/build-with-mob-action@master
+ with:
+ # skip build because we are going to build both Debug and RelWithDebInfo here
+ mo2-skip-build: true
+
+ # 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
+ 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/libs/uibase/.github/workflows/linting.yml b/libs/uibase/.github/workflows/linting.yml
new file mode 100644
index 0000000..67447b2
--- /dev/null
+++ b/libs/uibase/.github/workflows/linting.yml
@@ -0,0 +1,17 @@
+name: Lint UIBase
+
+on:
+ push:
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Check format
+ uses: ModOrganizer2/check-formatting-action@master
+ with:
+ check-path: "."
+ exclude-regex: "third-party"
diff --git a/libs/uibase/.gitignore b/libs/uibase/.gitignore
new file mode 100644
index 0000000..a55b4ba
--- /dev/null
+++ b/libs/uibase/.gitignore
@@ -0,0 +1,9 @@
+edit
+.vscode
+CMakeLists.txt.user
+/msbuild.log
+/*std*.log
+/*build
+/src/uibasetests_en.ts
+/install
+/tests/cmake/build
diff --git a/libs/uibase/.pre-commit-config.yaml b/libs/uibase/.pre-commit-config.yaml
new file mode 100644
index 0000000..8f53884
--- /dev/null
+++ b/libs/uibase/.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: v20.1.7
+ 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/libs/uibase/CMakeLists.txt b/libs/uibase/CMakeLists.txt
new file mode 100644
index 0000000..e9be7bb
--- /dev/null
+++ b/libs/uibase/CMakeLists.txt
@@ -0,0 +1,34 @@
+cmake_minimum_required(VERSION 3.16)
+
+include(CMakePackageConfigHelpers)
+
+project(uibase VERSION 2.5.3)
+
+# Standalone build - bypass mo2-cmake
+add_subdirectory(src)
+
+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
+)
+
+write_basic_package_version_file(
+ "${CMAKE_CURRENT_BINARY_DIR}/mo2-uibase-config-version.cmake"
+ VERSION "${PROJECT_VERSION}"
+ 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/libs/uibase/CMakePresets.json b/libs/uibase/CMakePresets.json
new file mode 100644
index 0000000..6293933
--- /dev/null
+++ b/libs/uibase/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/libs/uibase/README.md b/libs/uibase/README.md
new file mode 100644
index 0000000..b9abeaa
--- /dev/null
+++ b/libs/uibase/README.md
@@ -0,0 +1,56 @@
+# 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)](<https://github.com/ModOrganizer2/modorganizer-uibase/actions>)
+
+## 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/libs/uibase/cmake/config.cmake.in b/libs/uibase/cmake/config.cmake.in
new file mode 100644
index 0000000..d6d7899
--- /dev/null
+++ b/libs/uibase/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/libs/uibase/include/uibase/delayedfilewriter.h b/libs/uibase/include/uibase/delayedfilewriter.h
new file mode 100644
index 0000000..ecdfd8d
--- /dev/null
+++ b/libs/uibase/include/uibase/delayedfilewriter.h
@@ -0,0 +1,74 @@
+#ifndef DELAYEDFILEWRITER_H
+#define DELAYEDFILEWRITER_H
+
+#include "dllimport.h"
+#include <QString>
+#include <QTimer>
+#include <functional>
+
+namespace MOBase
+{
+
+/**
+ * The purpose of this class is to aggregate changes to a file before writing it out
+ */
+class QDLLEXPORT DelayedFileWriterBase : public QObject
+{
+
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ * @param fileName
+ * @param delay delay (in milliseconds) before we call the actual write function
+ */
+ DelayedFileWriterBase(int delay = 200);
+ ~DelayedFileWriterBase();
+
+public slots:
+ /**
+ * @brief write with delay
+ */
+ void write();
+
+ /**
+ * @brief cancel a scheduled write (does nothing if no write is scheduled)
+ */
+ void cancel();
+
+ /**
+ * @brief write immediately without waiting for the timer to expire
+ * @param ifOnTimer only write if the timer is running
+ */
+ void writeImmediately(bool ifOnTimer);
+
+private slots:
+ void timerExpired();
+
+private:
+ virtual void doWrite() = 0;
+
+private:
+ int m_TimerDelay;
+ QTimer m_Timer;
+};
+
+class QDLLEXPORT DelayedFileWriter : public DelayedFileWriterBase
+{
+public:
+ typedef std::function<void()> WriterFunc;
+
+public:
+ DelayedFileWriter(WriterFunc func, int delay = 200);
+
+private:
+ void doWrite();
+
+private:
+ WriterFunc m_Func;
+};
+
+} // namespace MOBase
+
+#endif // DELAYEDFILEWRITER_H
diff --git a/libs/uibase/include/uibase/diagnosisreport.h b/libs/uibase/include/uibase/diagnosisreport.h
new file mode 100644
index 0000000..4b541ff
--- /dev/null
+++ b/libs/uibase/include/uibase/diagnosisreport.h
@@ -0,0 +1,51 @@
+/*
+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 DIAGNOSISREPORT_H
+#define DIAGNOSISREPORT_H
+
+#include <QString>
+
+namespace MOBase
+{
+
+/**
+ * @brief report for a single problem reported by a plugin
+ */
+struct ProblemReport
+{
+ QString key; // a plugin-defined unique key for the issue. This is used to refer to
+ // the problem
+ enum
+ {
+ SEVERITY_REPORT, // the issue should be reported but nothing more
+ SEVERITY_BREAKPLUGIN, // the issue breaks the plugin (the plugin has to disable
+ // itself)
+ SEVERITY_BREAKGAME // the issue will (likely) break the game. The user will be
+ // warned about this every time he tries to start
+ } severity;
+ bool guidedFix; // if true, the plugin provides a guide to fixing the issue
+ QString shortDescription; // short description text for the overview
+ QString longDescription; //
+};
+
+} // namespace MOBase
+
+#endif // DIAGNOSISREPORT_H
diff --git a/libs/uibase/include/uibase/dllimport.h b/libs/uibase/include/uibase/dllimport.h
new file mode 100644
index 0000000..577829d
--- /dev/null
+++ b/libs/uibase/include/uibase/dllimport.h
@@ -0,0 +1,46 @@
+/*
+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 DLLIMPORT_H
+#define DLLIMPORT_H
+
+namespace MOBase
+{
+
+#ifdef _WIN32
+ #if defined(UIBASE_EXPORT)
+ #define QDLLEXPORT __declspec(dllexport)
+ #elif defined(_NODLL)
+ #define QDLLEXPORT
+ #else
+ #undef DLLEXPORT
+ #define QDLLEXPORT __declspec(dllimport)
+ #endif
+#else
+ #if defined(UIBASE_EXPORT)
+ #define QDLLEXPORT __attribute__((visibility("default")))
+ #else
+ #define QDLLEXPORT
+ #endif
+#endif
+
+} // namespace MOBase
+
+#endif // DLLIMPORT_H
diff --git a/libs/uibase/include/uibase/errorcodes.h b/libs/uibase/include/uibase/errorcodes.h
new file mode 100644
index 0000000..e880ce0
--- /dev/null
+++ b/libs/uibase/include/uibase/errorcodes.h
@@ -0,0 +1,21 @@
+#ifndef UIBASE_ERRORCODES_H
+#define UIBASE_ERRORCODES_H
+
+#include "dllimport.h"
+#include <cstdint>
+
+#ifdef _WIN32
+#include <Windows.h>
+#else
+// POSIX: use uint32_t as DWORD equivalent
+using DWORD = uint32_t;
+#endif
+
+namespace MOBase
+{
+
+QDLLEXPORT const wchar_t* errorCodeName(DWORD code);
+
+} // namespace MOBase
+
+#endif // UIBASE_ERRORCODES_H
diff --git a/libs/uibase/include/uibase/eventfilter.h b/libs/uibase/include/uibase/eventfilter.h
new file mode 100644
index 0000000..3878a50
--- /dev/null
+++ b/libs/uibase/include/uibase/eventfilter.h
@@ -0,0 +1,45 @@
+/*
+Copyright (C) 2016 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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+#include "dllimport.h"
+#include <QObject>
+#include <functional>
+
+namespace MOBase
+{
+
+class QDLLEXPORT EventFilter : public QObject
+{
+
+ Q_OBJECT
+
+ typedef std::function<bool(QObject*, QEvent*)> HandlerFunc;
+
+public:
+ EventFilter(QObject* parent, const HandlerFunc& handler);
+
+ virtual bool eventFilter(QObject* obj, QEvent* event) override;
+
+private:
+ HandlerFunc m_Handler;
+};
+
+} // namespace MOBase
diff --git a/libs/uibase/include/uibase/exceptions.h b/libs/uibase/include/uibase/exceptions.h
new file mode 100644
index 0000000..58b0d34
--- /dev/null
+++ b/libs/uibase/include/uibase/exceptions.h
@@ -0,0 +1,58 @@
+#ifndef UIBASE_EXCEPTIONS_H
+#define UIBASE_EXCEPTIONS_H
+
+#include <stdexcept>
+
+#include <QObject>
+#include <QString>
+
+#include "dllimport.h"
+
+namespace MOBase
+{
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4275) // non-dll interface
+#endif
+
+/**
+ * @brief exception class that takes a QString as the parameter
+ **/
+class QDLLEXPORT Exception : public std::exception
+{
+public:
+ Exception(const QString& text) : m_Message(text.toUtf8()) {}
+
+ virtual const char* what() const noexcept override { return m_Message.constData(); }
+
+private:
+ QByteArray m_Message;
+};
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+// Exception thrown in case of incompatibilities, i.e. between plugins.
+class QDLLEXPORT IncompatibilityException : public Exception
+{
+public:
+ using Exception::Exception;
+};
+
+// Exception thrown for invalid NXM links.
+class QDLLEXPORT InvalidNXMLinkException : public Exception
+{
+public:
+ InvalidNXMLinkException(const QString& link)
+ : Exception(QObject::tr("invalid nxm-link: %1").arg(link))
+ {}
+};
+
+// alias for backward-compatibility, should be removed when possible
+using MyException = Exception;
+
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/executableinfo.h b/libs/uibase/include/uibase/executableinfo.h
new file mode 100644
index 0000000..1fe52b3
--- /dev/null
+++ b/libs/uibase/include/uibase/executableinfo.h
@@ -0,0 +1,67 @@
+#ifndef EXECUTABLEINFO_H
+#define EXECUTABLEINFO_H
+
+#include "dllimport.h"
+#include <QDir>
+#include <QFileInfo>
+#include <QList>
+#include <QString>
+
+namespace MOBase
+{
+
+class QDLLEXPORT ExecutableForcedLoadSetting
+{
+public:
+ ExecutableForcedLoadSetting(const QString& process, const QString& library);
+
+ ExecutableForcedLoadSetting& withForced(bool forced = true);
+
+ ExecutableForcedLoadSetting& withEnabled(bool enabled = true);
+
+ bool enabled() const;
+ bool forced() const;
+ QString library() const;
+ QString process() const;
+
+private:
+ bool m_Enabled;
+ QString m_Process;
+ QString m_Library;
+ bool m_Forced;
+};
+
+class QDLLEXPORT ExecutableInfo
+{
+public:
+ ExecutableInfo(const QString& title, const QFileInfo& binary);
+
+ ExecutableInfo& withArgument(const QString& argument);
+
+ ExecutableInfo& withWorkingDirectory(const QDir& workingDirectory);
+
+ ExecutableInfo& withSteamAppId(const QString& appId);
+
+ ExecutableInfo& asCustom();
+
+ bool isValid() const;
+
+ QString title() const;
+ QFileInfo binary() const;
+ QStringList arguments() const;
+ QDir workingDirectory() const;
+ QString steamAppID() const;
+ bool isCustom() const;
+
+private:
+ QString m_Title;
+ QFileInfo m_Binary;
+ QStringList m_Arguments;
+ QDir m_WorkingDirectory;
+ QString m_SteamAppID;
+ bool m_Custom{false};
+};
+
+} // namespace MOBase
+
+#endif // EXECUTABLEINFO_H
diff --git a/libs/uibase/include/uibase/expanderwidget.h b/libs/uibase/include/uibase/expanderwidget.h
new file mode 100644
index 0000000..0a5b07f
--- /dev/null
+++ b/libs/uibase/include/uibase/expanderwidget.h
@@ -0,0 +1,63 @@
+#ifndef EXPANDERWIDGET_H
+#define EXPANDERWIDGET_H
+
+#include "dllimport.h"
+#include <QToolButton>
+
+namespace MOBase
+{
+
+/* Takes a QToolButton and a widget and creates an expandable widget.
+ **/
+class QDLLEXPORT ExpanderWidget : public QObject
+{
+ Q_OBJECT;
+
+public:
+ /** empty expander, use set()
+ **/
+ ExpanderWidget();
+
+ /** see set()
+ **/
+ ExpanderWidget(QToolButton* button, QWidget* content);
+
+ /** @brief sets the button and content widgets to use
+ * the button will be given an arrow icon, clicking it will toggle the
+ * visibility of the given widget
+ * @param button the button that toggles the content
+ * @param content the widget that will be shown or hidden
+ * @param opened initial state, defaults to closed
+ **/
+ void set(QToolButton* button, QWidget* content, bool opened = false);
+
+ /** either opens or closes the expander depending on the current state
+ **/
+ void toggle();
+
+ /** sets the current state of the expander
+ **/
+ void toggle(bool b);
+
+ /** returns whether the expander is currently opened
+ **/
+ bool opened() const;
+
+ QByteArray saveState() const;
+ void restoreState(const QByteArray& a);
+
+ QToolButton* button() const;
+
+signals:
+ void aboutToToggle(bool b);
+ void toggled(bool b);
+
+private:
+ QToolButton* m_button;
+ QWidget* m_content;
+ bool opened_;
+};
+
+} // namespace MOBase
+
+#endif // EXPANDERWIDGET_H
diff --git a/libs/uibase/include/uibase/filemapping.h b/libs/uibase/include/uibase/filemapping.h
new file mode 100644
index 0000000..2498c10
--- /dev/null
+++ b/libs/uibase/include/uibase/filemapping.h
@@ -0,0 +1,17 @@
+#ifndef FILEMAPPING_H
+#define FILEMAPPING_H
+
+#include <QString>
+#include <vector>
+
+struct Mapping
+{
+ QString source;
+ QString destination;
+ bool isDirectory;
+ bool createTarget;
+};
+
+typedef std::vector<Mapping> MappingType;
+
+#endif // FILEMAPPING_H
diff --git a/libs/uibase/include/uibase/filesystemutilities.h b/libs/uibase/include/uibase/filesystemutilities.h
new file mode 100644
index 0000000..ae074c6
--- /dev/null
+++ b/libs/uibase/include/uibase/filesystemutilities.h
@@ -0,0 +1,36 @@
+#ifndef FILESYSTEMUTILIITES_H
+#define FILESYSTEMUTILITIES_H
+
+#include <QString>
+
+#include "dllimport.h"
+
+namespace MOBase
+{
+/**
+ * @brief fix a directory name so it can be dealt with by windows explorer
+ * @return false if there was no way to convert the name into a valid one
+ **/
+QDLLEXPORT bool fixDirectoryName(QString& name);
+
+/**
+ * @brief ensures a file name is valid
+ *
+ * @param name the file name being sanitized
+ * @param replacement invalid characters are replaced with this string
+ * @return the sanitized file name
+ **/
+QDLLEXPORT QString sanitizeFileName(const QString& name,
+ const QString& replacement = QString(""));
+
+/**
+ * @brief checks file name validity per sanitizeFileName
+ *
+ * @param name the file name being checked
+ * @return true if the given file name is valid
+ **/
+QDLLEXPORT bool validFileName(const QString& name);
+
+} // namespace MOBase
+
+#endif // FILESYSTEM_H
diff --git a/libs/uibase/include/uibase/filterwidget.h b/libs/uibase/include/uibase/filterwidget.h
new file mode 100644
index 0000000..b6f09cd
--- /dev/null
+++ b/libs/uibase/include/uibase/filterwidget.h
@@ -0,0 +1,148 @@
+#ifndef FILTERWIDGET_H
+#define FILTERWIDGET_H
+
+#include "dllimport.h"
+#include <QAbstractItemView>
+#include <QLineEdit>
+#include <QList>
+#include <QObject>
+#include <QRegularExpression>
+#include <QShortcut>
+#include <QSortFilterProxyModel>
+#include <QToolButton>
+
+namespace MOBase
+{
+
+class EventFilter;
+class FilterWidget;
+
+class QDLLEXPORT FilterWidgetProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT;
+
+public:
+ FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent = nullptr);
+ void refreshFilter() {
+#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
+ beginFilterChange();
+#endif
+ invalidateFilter();
+ }
+
+protected:
+ bool filterAcceptsRow(int row, const QModelIndex& parent) const override;
+ void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
+
+private:
+ FilterWidget& m_filter;
+
+ bool columnMatches(int sourceRow, const QModelIndex& sourceParent, int c,
+ const QRegularExpression& what) const;
+};
+
+class QDLLEXPORT FilterWidget : public QObject
+{
+ Q_OBJECT;
+
+public:
+ struct Options
+ {
+ bool useRegex = false;
+ bool regexCaseSensitive = false;
+ bool regexExtended = false;
+ bool scrollToSelection = false;
+ };
+
+ using predFun = std::function<bool(const QRegularExpression& what)>;
+ using sortFun = std::function<bool(const QModelIndex&, const QModelIndex&)>;
+
+ FilterWidget();
+
+ static void setOptions(const Options& o);
+ static Options options();
+
+ void setEdit(QLineEdit* edit);
+ void setList(QAbstractItemView* list);
+ void clear();
+ void scrollToSelection();
+ bool empty() const;
+
+ void setUpdateDelay(bool b);
+ bool hasUpdateDelay() const;
+
+ void setUseSourceSort(bool b);
+ bool useSourceSort() const;
+
+ void setSortPredicate(sortFun f);
+ const sortFun& sortPredicate() const;
+
+ void setFilterColumn(int i);
+ int filterColumn() const;
+
+ void setFilteringEnabled(bool b);
+ bool filteringEnabled() const;
+
+ void setFilteredBorder(bool b);
+ bool filteredBorder() const;
+
+ FilterWidgetProxyModel* proxyModel();
+ QAbstractItemModel* sourceModel();
+
+ QModelIndex mapFromSource(const QModelIndex& index) const;
+ QModelIndex mapToSource(const QModelIndex& index) const;
+ QItemSelection mapSelectionFromSource(const QItemSelection& sel) const;
+ QItemSelection mapSelectionToSource(const QItemSelection& sel) const;
+
+ bool matches(predFun pred) const;
+ bool matches(const QString& s) const;
+
+signals:
+ void aboutToChange(const QString& oldFilter, const QString& newFilter);
+ void changed(const QString& oldFilter, const QString& newFilter);
+
+private:
+ using Compiled = QList<QList<QRegularExpression>>;
+
+ QLineEdit* m_edit;
+ QAbstractItemView* m_list;
+ FilterWidgetProxyModel* m_proxy;
+ EventFilter* m_eventFilter;
+ QToolButton* m_clear;
+ QString m_text;
+ Compiled m_compiled;
+ QTimer* m_timer;
+ std::vector<QShortcut*> m_shortcuts;
+ bool m_useDelay;
+ bool m_valid;
+ bool m_useSourceSort;
+ sortFun m_lt;
+ int m_filterColumn;
+ bool m_filteringEnabled;
+ bool m_filteredBorder;
+
+ void hookEdit();
+ void unhookEdit();
+
+ void hookList();
+ void setShortcuts();
+ void unhookList();
+
+ void createClear();
+ void repositionClearButton();
+
+ void onTextChanged();
+ void onFind();
+ void onReset();
+ void onResized();
+ void onContextMenu(QObject*, QContextMenuEvent* e);
+
+ void set();
+ void update();
+ void compile();
+};
+
+} // namespace MOBase
+
+#endif // FILTERWIDGET_H
diff --git a/libs/uibase/include/uibase/finddialog.h b/libs/uibase/include/uibase/finddialog.h
new file mode 100644
index 0000000..77a8ac3
--- /dev/null
+++ b/libs/uibase/include/uibase/finddialog.h
@@ -0,0 +1,80 @@
+/*
+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 FINDDIALOG_H
+#define FINDDIALOG_H
+
+#include <QDialog>
+
+namespace Ui
+{
+class FindDialog;
+}
+
+namespace MOBase
+{
+
+/**
+ * @brief Find dialog used in the TextView dialog
+ **/
+class FindDialog : public QDialog
+{
+
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ *
+ * @param parent parent widget
+ **/
+ explicit FindDialog(QWidget* parent = 0);
+
+ ~FindDialog();
+
+signals:
+
+ /**
+ * @brief emitted when the user wants to jump to the next location matching the
+ *pattern
+ **/
+ void findNext();
+
+ /**
+ * @brief emitted when the user changes the pattern to search for
+ *
+ * @param pattern the new search pattern
+ **/
+ void patternChanged(const QString& pattern);
+
+private slots:
+ void on_nextBtn_clicked();
+
+ void on_patternEdit_textChanged(const QString& arg1);
+
+ void on_closeBtn_clicked();
+
+private:
+ Ui::FindDialog* ui;
+};
+
+} // namespace MOBase
+
+#endif // FINDDIALOG_H
diff --git a/libs/uibase/include/uibase/formatters.h b/libs/uibase/include/uibase/formatters.h
new file mode 100644
index 0000000..e33aa86
--- /dev/null
+++ b/libs/uibase/include/uibase/formatters.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "./formatters/enums.h"
+#include "./formatters/qt.h"
+#include "./formatters/strings.h"
diff --git a/libs/uibase/include/uibase/formatters/enums.h b/libs/uibase/include/uibase/formatters/enums.h
new file mode 100644
index 0000000..6538140
--- /dev/null
+++ b/libs/uibase/include/uibase/formatters/enums.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <format>
+#include <type_traits>
+
+template <class Enum, class CharT>
+ requires std::is_enum_v<Enum>
+struct std::formatter<Enum, CharT> : std::formatter<std::underlying_type_t<Enum>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(Enum v, FmtContext& ctx) const
+ {
+ return std::formatter<std::underlying_type_t<Enum>, CharT>::format(
+ static_cast<std::underlying_type_t<Enum>>(v), ctx);
+ }
+};
diff --git a/libs/uibase/include/uibase/formatters/qt.h b/libs/uibase/include/uibase/formatters/qt.h
new file mode 100644
index 0000000..de64aa8
--- /dev/null
+++ b/libs/uibase/include/uibase/formatters/qt.h
@@ -0,0 +1,87 @@
+#pragma once
+
+#include <format>
+
+#include <QColor>
+#include <QFlag>
+#include <QFlags>
+#include <QRect>
+#include <QSize>
+#include <QString>
+#include <QVariant>
+
+template <class CharT>
+struct std::formatter<QSize, CharT> : std::formatter<std::basic_string<CharT>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QSize s, FmtContext& ctx) const
+ {
+ return std::format_to(ctx.out(), "QSize({}, {})", s.width(), s.height());
+ }
+};
+
+template <class CharT>
+struct std::formatter<QRect, CharT> : std::formatter<std::basic_string<CharT>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QRect r, FmtContext& ctx) const
+ {
+ return std::format_to(ctx.out(), "QRect({},{}-{},{})", r.left(), r.top(), r.right(),
+ r.bottom());
+ }
+};
+
+template <class CharT>
+struct std::formatter<QColor, CharT> : std::formatter<std::basic_string<CharT>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QColor c, FmtContext& ctx) const
+ {
+ return std::format_to(ctx.out(), "QColor({}, {}, {}, {})", c.red(), c.green(),
+ c.blue(), c.alpha());
+ }
+};
+
+template <class CharT>
+struct std::formatter<QByteArray, CharT>
+ : std::formatter<std::basic_string<CharT>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QByteArray v, FmtContext& ctx) const
+ {
+ return std::format_to(ctx.out(), "QByteArray({} bytes)", v.size());
+ }
+};
+
+template <class CharT>
+struct std::formatter<QVariant, CharT> : std::formatter<std::basic_string<CharT>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QVariant v, FmtContext& ctx) const
+ {
+ return std::format_to(
+ ctx.out(), "QVariant(type={}, value={})", v.typeName(),
+ (v.typeId() == QMetaType::Type::QByteArray ? "(binary)" : v.toString()));
+ }
+};
+
+template <class CharT>
+struct std::formatter<QFlag, CharT> : std::formatter<int, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QFlag v, FmtContext& ctx) const
+ {
+ return std::formatter<int, CharT>::format(static_cast<int>(v), ctx);
+ }
+};
+
+template <class T, class CharT>
+struct std::formatter<QFlags<T>, CharT> : std::formatter<int, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QFlags<T> v, FmtContext& ctx) const
+ {
+ // TODO: display flags has aa | bb | cc?
+ return std::formatter<int, CharT>::format(v.toInt(), ctx);
+ }
+};
diff --git a/libs/uibase/include/uibase/formatters/strings.h b/libs/uibase/include/uibase/formatters/strings.h
new file mode 100644
index 0000000..bc31ef9
--- /dev/null
+++ b/libs/uibase/include/uibase/formatters/strings.h
@@ -0,0 +1,89 @@
+
+#pragma once
+
+#include <format>
+#include <string>
+#include <string_view>
+
+#include <QString>
+#include <QStringView>
+
+namespace MOBase::details
+{
+template <class CharT>
+inline std::basic_string<CharT> toStdBasicString(QString const& qstring);
+
+template <>
+inline std::basic_string<char> toStdBasicString(QString const& qstring)
+{
+ return qstring.toStdString();
+}
+template <>
+inline std::basic_string<wchar_t> toStdBasicString(QString const& qstring)
+{
+ return qstring.toStdWString();
+}
+template <>
+inline std::basic_string<char16_t> toStdBasicString(QString const& qstring)
+{
+ return qstring.toStdU16String();
+}
+template <>
+inline std::basic_string<char32_t> toStdBasicString(QString const& qstring)
+{
+ return qstring.toStdU32String();
+}
+
+inline QString fromStdBasicString(std::string const& value)
+{
+ return QString::fromStdString(value);
+}
+inline QString fromStdBasicString(std::wstring const& value)
+{
+ return QString::fromStdWString(value);
+}
+inline QString fromStdBasicString(std::u16string const& value)
+{
+ return QString::fromStdU16String(value);
+}
+inline QString fromStdBasicString(std::u32string const& value)
+{
+ return QString::fromStdU32String(value);
+}
+} // namespace MOBase::details
+
+template <class CharT>
+struct std::formatter<QString, CharT> : std::formatter<std::basic_string<CharT>, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QString s, FmtContext& ctx) const
+ {
+ return std::formatter<std::basic_string<CharT>, CharT>::format(
+ MOBase::details::toStdBasicString<CharT>(s), ctx);
+ }
+};
+
+template <class CharT1, class CharT2>
+ requires(!std::is_same_v<CharT1, CharT2>)
+struct std::formatter<std::basic_string<CharT1>, CharT2>
+ : std::formatter<std::basic_string<CharT2>, CharT2>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(std::basic_string<CharT1> s, FmtContext& ctx) const
+ {
+ return std::formatter<std::basic_string<CharT2>, CharT2>::format(
+ MOBase::details::toStdBasicString<CharT2>(
+ MOBase::details::fromStdBasicString(s)),
+ ctx);
+ }
+};
+
+template <class CharT>
+struct std::formatter<QStringView, CharT> : std::formatter<QString, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(QStringView s, FmtContext& ctx) const
+ {
+ return std::formatter<QString, CharT>::format(s.toString(), ctx);
+ }
+};
diff --git a/libs/uibase/include/uibase/game_features/bsainvalidation.h b/libs/uibase/include/uibase/game_features/bsainvalidation.h
new file mode 100644
index 0000000..926753d
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/bsainvalidation.h
@@ -0,0 +1,26 @@
+#ifndef UIBASE_GAMEFEATURES_BSAINVALIDATION_H
+#define UIBASE_GAMEFEATURES_BSAINVALIDATION_H
+
+#include <QString>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+class IProfile;
+
+class BSAInvalidation : public details::GameFeatureCRTP<BSAInvalidation>
+{
+public:
+ virtual bool isInvalidationBSA(const QString& bsaName) = 0;
+
+ virtual void deactivate(MOBase::IProfile* profile) = 0;
+
+ virtual void activate(MOBase::IProfile* profile) = 0;
+
+ virtual bool prepareProfile(MOBase::IProfile* profile) = 0;
+};
+
+} // namespace MOBase
+
+#endif // BSAINVALIDATION_H
diff --git a/libs/uibase/include/uibase/game_features/dataarchives.h b/libs/uibase/include/uibase/game_features/dataarchives.h
new file mode 100644
index 0000000..65c1419
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/dataarchives.h
@@ -0,0 +1,35 @@
+#ifndef UIBASE_GAMEFEATURES_DATAARCHIVES_H
+#define UIBASE_GAMEFEATURES_DATAARCHIVES_H
+
+#include <QString>
+#include <QStringList>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+class IProfile;
+
+class DataArchives : public details::GameFeatureCRTP<DataArchives>
+{
+public:
+ virtual QStringList vanillaArchives() const = 0;
+
+ virtual QStringList archives(const MOBase::IProfile* profile) const = 0;
+
+ /**
+ * @brief add an archive to the archive list
+ * @param profile the profile for which to change the archive list
+ * @param index index to insert before. 0 is the beginning of the list, INT_MAX can be
+ * used for the end of the list
+ * @param archiveName the archive to add
+ */
+ virtual void addArchive(MOBase::IProfile* profile, int index,
+ const QString& archiveName) = 0;
+
+ virtual void removeArchive(MOBase::IProfile* profile, const QString& archiveName) = 0;
+};
+
+} // namespace MOBase
+
+#endif // DATAARCHIVES
diff --git a/libs/uibase/include/uibase/game_features/game_feature.h b/libs/uibase/include/uibase/game_features/game_feature.h
new file mode 100644
index 0000000..6e0dd4e
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/game_feature.h
@@ -0,0 +1,40 @@
+#ifndef UIBASE_GAMEFEATURES_GAMEFEATURE_H
+#define UIBASE_GAMEFEATURES_GAMEFEATURE_H
+
+#include <typeindex>
+
+namespace MOBase
+{
+
+/**
+ * Empty class that is inherit by all game features.
+ */
+class GameFeature
+{
+public:
+ GameFeature() = default;
+ virtual ~GameFeature() = 0;
+
+ /**
+ * @brief Retrieve the type index of the main game feature this feature extends.
+ */
+ virtual const std::type_info& typeInfo() const = 0;
+};
+
+// Pure virtual destructor must still have a definition
+inline GameFeature::~GameFeature() {}
+
+namespace details
+{
+
+ template <class T>
+ class GameFeatureCRTP : public GameFeature
+ {
+ const std::type_info& typeInfo() const final { return typeid(T); }
+ };
+
+} // namespace details
+
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/game_features/gameplugins.h b/libs/uibase/include/uibase/game_features/gameplugins.h
new file mode 100644
index 0000000..3406d91
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/gameplugins.h
@@ -0,0 +1,25 @@
+#ifndef UIBASE_GAMEFEATURES_GAMEPLUGINS_H
+#define UIBASE_GAMEFEATURES_GAMEPLUGINS_H
+
+#include <QStringList>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+class IPluginList;
+
+class GamePlugins : public details::GameFeatureCRTP<GamePlugins>
+{
+public:
+ virtual void writePluginLists(const MOBase::IPluginList* pluginList) = 0;
+ virtual void readPluginLists(MOBase::IPluginList* pluginList) = 0;
+ virtual QStringList getLoadOrder() = 0;
+ virtual bool lightPluginsAreSupported() { return false; }
+ virtual bool mediumPluginsAreSupported() { return false; }
+ virtual bool blueprintPluginsAreSupported() { return false; }
+};
+
+} // namespace MOBase
+
+#endif // GAMEPLUGINS_H
diff --git a/libs/uibase/include/uibase/game_features/igamefeatures.h b/libs/uibase/include/uibase/game_features/igamefeatures.h
new file mode 100644
index 0000000..2d89b3c
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/igamefeatures.h
@@ -0,0 +1,166 @@
+#ifndef UIBASE_GAMEFEATURES_IGAMEFEATURES_H
+#define UIBASE_GAMEFEATURES_IGAMEFEATURES_H
+
+#include <memory>
+#include <tuple>
+
+#include <QStringList>
+
+#include "game_feature.h"
+
+namespace MOBase
+{
+
+class IPluginGame;
+
+// top-level game features
+class BSAInvalidation;
+class DataArchives;
+class GamePlugins;
+class LocalSavegames;
+class ModDataChecker;
+class ModDataContent;
+class SaveGameInfo;
+class ScriptExtender;
+class UnmanagedMods;
+
+namespace details
+{
+
+ // use pointers in the tuple since are only forward-declaring the features here
+ using BaseGameFeaturesP =
+ std::tuple<BSAInvalidation*, DataArchives*, GamePlugins*, LocalSavegames*,
+ ModDataChecker*, ModDataContent*, SaveGameInfo*, ScriptExtender*,
+ UnmanagedMods*>;
+
+} // namespace details
+
+// simple concept that only restricting template function that should take a game
+// feature to actually viable game feature types
+//
+template <class T>
+concept BaseGameFeature = requires(T) {
+ {
+ std::get<T*>(std::declval<details::BaseGameFeaturesP>())
+ } -> std::convertible_to<T*>;
+};
+
+/**
+ * @brief Interface to game features.
+ *
+ */
+class IGameFeatures
+{
+public:
+ /**
+ * @brief Register game feature for the specified game.
+ *
+ * This method register a game feature to combine or replace the same feature provided
+ * by the game. Some features are merged (e.g., ModDataContent, ModDataChecker), while
+ * other override previous features (e.g., SaveGameInfo).
+ *
+ * For features that can be combined, the priority argument indicates the order of
+ * priority (e.g., the order of the checks for ModDataChecker). For other features,
+ * the feature with the highest priority will be used. The features provided by the
+ * game plugin itself always have lowest priority.
+ *
+ * The feature is associated to the plugin that registers it, if the plugin is
+ * disabled, the feature will not be available.
+ *
+ * This function will return True if the feature was registered, even if the feature
+ * is not used du to its low priority.
+ *
+ * @param games Names of the game to enable the feature for.
+ * @param feature Game feature to register.
+ * @param priority Priority of the game feature. If the plugin registering the feature
+ * is a game plugin, this parameter is ignored.
+ * @param replace If true, remove features of the same kind registered by the current
+ * plugin, otherwise add the feature alongside existing ones.
+ *
+ * @return true if the game feature was properly registered, false otherwise.
+ */
+ virtual bool registerFeature(QStringList const& games,
+ std::shared_ptr<GameFeature> feature, int priority,
+ bool replace = false) = 0;
+
+ /**
+ * @brief Register game feature for the specified game.
+ *
+ * See first overload for more details.
+ *
+ * @param game Game to enable the feature for.
+ * @param feature Game feature to register.
+ * @param priority Priority of the game feature.
+ * @param replace If true, remove features of the same kind registered by the current
+ * plugin, otherwise add the feature alongside existing ones.
+ *
+ * @return true if the game feature was properly registered, false otherwise.
+ */
+ virtual bool registerFeature(IPluginGame* game, std::shared_ptr<GameFeature> feature,
+ int priority, bool replace = false) = 0;
+
+ /**
+ * @brief Register game feature for all games.
+ *
+ * See first overload for more details.
+ *
+ * @param feature Game feature to register.
+ * @param priority Priority of the game feature.
+ * @param replace If true, remove features of the same kind registered by the current
+ * plugin, otherwise add the feature alongside existing ones.
+ *
+ * @return true if the game feature was properly registered, false otherwise.
+ */
+ virtual bool registerFeature(std::shared_ptr<GameFeature> feature, int priority,
+ bool replace = false) = 0;
+
+ /**
+ * @brief Unregister the given game feature.
+ *
+ * This function is safe to use even if the feature was not registered before.
+ *
+ * @param feature Feature to unregister.
+ */
+ virtual bool unregisterFeature(std::shared_ptr<GameFeature> feature) = 0;
+
+ /**
+ * @brief Unregister all features of the given type registered by the calling plugin.
+ *
+ * This function is safe to use even if the plugin has no feature of the given type
+ * register.
+ *
+ * @return the number of features unregistered.
+ *
+ * @tparam Feature Type of game feature to remove.
+ */
+ template <BaseGameFeature Feature>
+ int unregisterFeatures()
+ {
+ return unregisterFeaturesImpl(typeid(Feature));
+ }
+
+ /**
+ * Retrieve the given game feature, if one exists.
+ *
+ * @return the feature of the given type, if one exists, otherwise a null pointer.
+ */
+ template <BaseGameFeature T>
+ std::shared_ptr<T> gameFeature() const
+ {
+ // gameFeatureImpl ensure that the returned pointer is of the right type (or
+ // nullptr), so reinterpret_cast should be fine here
+ return std::dynamic_pointer_cast<T>(gameFeatureImpl(typeid(T)));
+ }
+
+public:
+ virtual ~IGameFeatures() = default;
+
+protected:
+ virtual std::shared_ptr<GameFeature>
+ gameFeatureImpl(std::type_info const& info) const = 0;
+ virtual int unregisterFeaturesImpl(std::type_info const& info) = 0;
+};
+
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/game_features/localsavegames.h b/libs/uibase/include/uibase/game_features/localsavegames.h
new file mode 100644
index 0000000..f425431
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/localsavegames.h
@@ -0,0 +1,21 @@
+#ifndef UIBASE_GAMEFEATURES_LOCALSAVEGAMES_H
+#define UIBASE_GAMEFEATURES_LOCALSAVEGAMES_H
+
+#include <QDir>
+
+#include "../filemapping.h"
+#include "./game_feature.h"
+
+namespace MOBase
+{
+class IProfile;
+
+class LocalSavegames : public details::GameFeatureCRTP<LocalSavegames>
+{
+public:
+ virtual MappingType mappings(const QDir& profileSaveDir) const = 0;
+ virtual bool prepareProfile(MOBase::IProfile* profile) = 0;
+};
+} // namespace MOBase
+
+#endif // LOCALSAVEGAMES_H
diff --git a/libs/uibase/include/uibase/game_features/moddatachecker.h b/libs/uibase/include/uibase/game_features/moddatachecker.h
new file mode 100644
index 0000000..4c95f79
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/moddatachecker.h
@@ -0,0 +1,66 @@
+#ifndef UIBASE_GAMEFEATURES_MODDATACHECKER_H
+#define UIBASE_GAMEFEATURES_MODDATACHECKER_H
+
+#include <memory>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+class IFileTree;
+
+class ModDataChecker : public details::GameFeatureCRTP<ModDataChecker>
+{
+public:
+ /**
+ *
+ */
+ enum class CheckReturn
+ {
+ INVALID,
+ FIXABLE,
+ VALID
+ };
+
+ /**
+ * @brief Check that the given filetree represent a valid mod layout, or can be easily
+ * fixed.
+ *
+ * This method is mainly used during installation (to find which installer should
+ * be used or to recurse into multi-level archives), or to quickly indicates to a
+ * user if a mod looks valid.
+ *
+ * This method does not have to be exact, it only has to indicate if the given tree
+ * looks like a valid mod or not by quickly checking the structure (heavy operations
+ * should be avoided).
+ *
+ * If the tree can be fixed by the `fix()` method, this method should return
+ * `FIXABLE`. `FIXABLE` should only be returned when it is guaranteed that `fix()` can
+ * fix the tree.
+ *
+ * @param tree The tree starting at the root of the "data" folder.
+ *
+ * @return whether the tree is invalid, fixable or valid.
+ */
+ virtual CheckReturn
+ dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const = 0;
+
+ /**
+ * @brief Try to fix the given tree.
+ *
+ * This method is used during installation to try to fix invalid archives and will
+ * only be called if dataLooksValid returned `FIXABLE`.
+ *
+ * @param tree The tree to try to fix. Can be modified during the process.
+ *
+ * @return the fixed tree, or a null pointer if the tree could not be fixed.
+ */
+ virtual std::shared_ptr<MOBase::IFileTree>
+ fix(std::shared_ptr<MOBase::IFileTree> fileTree) const
+ {
+ return nullptr;
+ }
+};
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/game_features/moddatacontent.h b/libs/uibase/include/uibase/game_features/moddatacontent.h
new file mode 100644
index 0000000..b86527c
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/moddatacontent.h
@@ -0,0 +1,122 @@
+#ifndef UIBASE_GAMEFEATURES_MODDATACONTENT_H
+#define UIBASE_GAMEFEATURES_MODDATACONTENT_H
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+#include <QString>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+class IFileTree;
+
+/**
+ * The ModDataContent feature is used (when available) to indicate to users the content
+ * of mods in the "Content" column.
+ *
+ * The feature exposes a list of possible content types, each associated with an ID, a
+ * name and an icon. The icon is the path to either:
+ * - A Qt resource or;
+ * - A file on the disk.
+ *
+ * In order to facilitate the implementation, MO2 already provides a set of icons that
+ * can be used. Those icons are all under :/MO/gui/content (e.g. :/MO/gui/content/plugin
+ * or :/MO/gui/content/music).
+ *
+ * The list of available icons is:
+ * - plugin:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/jigsaw-piece.png
+ * - skyproc:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/hand-of-god.png
+ * - texture:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/empty-chessboard.png
+ * - music:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/double-quaver.png
+ * - sound:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/lyre.png
+ * - interface:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/usable.png
+ * - skse:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/checkbox-tree.png
+ * - script:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/tinker.png
+ * - mesh:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/breastplate.png
+ * - string:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/conversation.png
+ * - bsa:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/locked-chest.png
+ * - menu:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/config.png
+ * - inifile:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/feather-and-scroll.png
+ * - modgroup:
+ * https://github.com/ModOrganizer2/modorganizer/blob/master/src/resources/contents/xedit.png
+ */
+class ModDataContent : public details::GameFeatureCRTP<ModDataContent>
+{
+public:
+ struct Content
+ {
+
+ /**
+ * @param id ID of this content.
+ * @param name Name of this content.
+ * @param icon Path to the icon for this content. Can be either a path
+ * to an image on the disk, or to a resource. Can be an empty string if
+ * filterOnly is true.
+ * @param filterOnly Indicates if the content should only be show in the filter
+ * criteria and not in the actual Content column.
+ */
+ Content(int id, QString name, QString icon, bool filterOnly = false)
+ : m_Id{id}, m_Name{name}, m_Icon{icon}, m_FilterOnly{filterOnly}
+ {}
+
+ /**
+ * @return the ID of this content.
+ */
+ int id() const { return m_Id; }
+
+ /**
+ * @return the name of this content.
+ */
+ QString name() const { return m_Name; }
+
+ /**
+ * @return the path to the icon of this content (can be a Qt resource path).
+ */
+ QString icon() const { return m_Icon; }
+
+ /**
+ * @return true if this content is only meant to be used as a filter criteria.
+ */
+ bool isOnlyForFilter() const { return m_FilterOnly; }
+
+ private:
+ int m_Id;
+ QString m_Name;
+ QString m_Icon;
+ bool m_FilterOnly;
+ };
+
+ /**
+ * @return the list of all possible contents for the corresponding game.
+ */
+ virtual std::vector<Content> getAllContents() const = 0;
+
+ /**
+ * @brief Retrieve the list of contents in the given tree.
+ *
+ * @param fileTree The tree corresponding to the mod to retrieve contents for.
+ *
+ * @return the IDs of the content in the given tree.
+ */
+ virtual std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const = 0;
+};
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/game_features/savegameinfo.h b/libs/uibase/include/uibase/game_features/savegameinfo.h
new file mode 100644
index 0000000..b5ea216
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/savegameinfo.h
@@ -0,0 +1,52 @@
+#ifndef UIBASE_GAMEFEATURES_SAVEGAMEINFO_H
+#define UIBASE_GAMEFEATURES_SAVEGAMEINFO_H
+
+#include <QMap>
+#include <QString>
+#include <QStringList>
+#include <QWidget>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+class ISaveGame;
+class ISaveGameInfoWidget;
+
+/** Feature to get hold of stuff to do with save games */
+class SaveGameInfo : public details::GameFeatureCRTP<SaveGameInfo>
+{
+public:
+ virtual ~SaveGameInfo() {}
+
+ typedef QStringList ProvidingModules;
+ typedef QMap<QString, ProvidingModules> MissingAssets;
+
+ /**
+ * @brief Get missing items from a save.
+ *
+ * @param save The save to retrieve missing assets from.
+ *
+ * @returns a collection of missing assets and the modules that can supply those
+ * assets.
+ *
+ * Note that in the situation where 'module' and 'asset' are indistinguishable,
+ * both still have to be supplied.
+ */
+ virtual MissingAssets getMissingAssets(MOBase::ISaveGame const& save) const = 0;
+
+ /**
+ * @brief Get a widget to display over the save game list.
+ *
+ * @param parent The parent widget.
+ *
+ * @returns a Qt widget to display saves Widget.
+ *
+ * It is permitted to return a null pointer to indicate the plugin does not have a
+ * nice visual way of displaying save game contents.
+ */
+ virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget* parent = 0) const = 0;
+};
+} // namespace MOBase
+
+#endif // SAVEGAMEINFO_H
diff --git a/libs/uibase/include/uibase/game_features/scriptextender.h b/libs/uibase/include/uibase/game_features/scriptextender.h
new file mode 100644
index 0000000..d6028ae
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/scriptextender.h
@@ -0,0 +1,53 @@
+#ifndef UIBASE_GAMEFEATURES_SCRIPTEXTENDER
+#define UIBASE_GAMEFEATURES_SCRIPTEXTENDER
+
+#include <cstdint>
+
+#ifdef _WIN32
+#include <windows.h>
+#else
+using WORD = uint16_t;
+#endif
+
+#include <QString>
+#include <QStringList>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+
+class ScriptExtender : public details::GameFeatureCRTP<ScriptExtender>
+{
+
+public:
+ virtual ~ScriptExtender() {}
+
+ /** Get the name of the script extender binary */
+ virtual QString BinaryName() const = 0;
+
+ /** Get the script extender plugin path*/
+ virtual QString PluginPath() const = 0;
+
+ /** The loader to use to ensure the game runs with the script extender */
+ virtual QString loaderName() const = 0;
+
+ /** Full path of the loader */
+ virtual QString loaderPath() const = 0;
+
+ /** Extension of the script extender save game */
+ virtual QString savegameExtension() const = 0;
+
+ /** Returns true if the extender is installed */
+ virtual bool isInstalled() const = 0;
+
+ /** Get version of extender */
+ virtual QString getExtenderVersion() const = 0;
+
+ /** Get CPU platform of extender */
+ virtual WORD getArch() const = 0;
+};
+
+} // namespace MOBase
+
+#endif // SCRIPTEXTENDER
diff --git a/libs/uibase/include/uibase/game_features/unmanagedmods.h b/libs/uibase/include/uibase/game_features/unmanagedmods.h
new file mode 100644
index 0000000..6eeb8a7
--- /dev/null
+++ b/libs/uibase/include/uibase/game_features/unmanagedmods.h
@@ -0,0 +1,45 @@
+#ifndef UIBASE_GAMEFEATURES_UNMANAGEDMODS_H
+#define UIBASE_GAMEFEATURES_UNMANAGEDMODS_H
+
+#include <QFileInfo>
+#include <QString>
+#include <QStringList>
+
+#include "./game_feature.h"
+
+namespace MOBase
+{
+
+class UnmanagedMods : public details::GameFeatureCRTP<UnmanagedMods>
+{
+public:
+ virtual ~UnmanagedMods() {}
+
+ /**
+ * @param onlyOfficial if set, only official mods (dlcs) are returned
+ * @return the list of unmanaged mods (internal names)
+ */
+ virtual QStringList mods(bool onlyOfficial) const = 0;
+
+ /**
+ * @param modName (internal) name of the mod being requested
+ * @return display name of the mod
+ */
+ virtual QString displayName(const QString& modName) const = 0;
+
+ /**
+ * @param modName name of the mod being requested
+ * @return reference file info
+ */
+ virtual QFileInfo referenceFile(const QString& modName) const = 0;
+
+ /**
+ * @param modName name of the mod being requested
+ * @return list of file names (absolute paths)
+ */
+ virtual QStringList secondaryFiles(const QString& modName) const = 0;
+};
+
+} // namespace MOBase
+
+#endif // UNMANAGEDMODS_H
diff --git a/libs/uibase/include/uibase/guessedvalue.h b/libs/uibase/include/uibase/guessedvalue.h
new file mode 100644
index 0000000..9c0be51
--- /dev/null
+++ b/libs/uibase/include/uibase/guessedvalue.h
@@ -0,0 +1,252 @@
+/*
+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 GUESSEDVALUE_H
+#define GUESSEDVALUE_H
+
+#include <functional>
+#include <set>
+
+namespace MOBase
+{
+
+/**
+ * @brief describes how good the code considers a guess (i.e. for a mod name)
+ * this is used to determine if a name from another source should overwrite or
+ * not
+ */
+enum EGuessQuality
+{
+ GUESS_INVALID, /// no valid value has been set yet
+ GUESS_FALLBACK, /// the guess is very basic and should only be used if no other
+ /// source is available
+ GUESS_GOOD, /// considered a good guess
+ GUESS_META, /// the value comes from meta data and is usually what the author
+ /// intended
+ GUESS_PRESET, /// the value comes from a previous install of the same data and
+ /// usually represents what the user chose before
+ GUESS_USER /// the user selection. Always overrules other sources
+};
+
+/**
+ * Represents a value that may be set from different places. Each time the value is
+ * changed a "quality" is specified to say how probable it is the value is the best
+ * choice. Only the best choice should be used in the end but alternatives can be
+ * queried. This class also allows a filter to be set. If a "guess" doesn't pass the
+ * filter, it is ignored.
+ */
+template <typename T>
+class GuessedValue
+{
+public:
+public:
+ /**
+ * @brief default constructor
+ */
+ GuessedValue();
+
+ /**
+ * @brief constructor with initial value
+ *
+ * @param reference the initial value to set
+ * @param quality quality of the guess
+ */
+ GuessedValue(const T& reference, EGuessQuality quality = GUESS_USER);
+
+ /**
+ * @brief
+ *
+ * @param reference
+ * @return GuessedValue<T>
+ */
+ GuessedValue<T>& operator=(const GuessedValue<T>& reference);
+
+ /**
+ * install a filter function. This filter is applied on every update and can
+ * refuse the update altogether or modify the value.
+ * @param filterFunction the filter to apply
+ */
+ /**
+ * @brief
+ *
+ * @param filterFunction
+ */
+ void setFilter(std::function<bool(T&)> filterFunction);
+
+ /**
+ * @brief
+ *
+ * @return operator const T
+ */
+ operator const T&() const { return m_Value; }
+
+ /**
+ * @brief
+ *
+ * @return T *operator ->
+ */
+ T* operator->() { return &m_Value; }
+ /**
+ * @brief
+ *
+ * @return const T *operator ->
+ */
+ /**
+ * @brief
+ *
+ * @return const T *operator ->
+ */
+ const T* operator->() const { return &m_Value; }
+
+ /**
+ * @brief
+ *
+ * @param value
+ * @return GuessedValue<T>
+ */
+ GuessedValue<T>& update(const T& value);
+ /**
+ * @brief
+ *
+ * @param value
+ * @param quality
+ * @return GuessedValue<T>
+ */
+ GuessedValue<T>& update(const T& value, EGuessQuality quality);
+
+ /**
+ * @brief
+ *
+ * @return const std::set<T>
+ */
+ const std::set<T>& variants() const { return m_Variants; }
+
+private:
+ T m_Value; /**< TODO */
+ std::set<T> m_Variants; /**< TODO */
+ EGuessQuality m_Quality; /**< TODO */
+ std::function<bool(T&)> m_Filter;
+};
+
+template <typename T>
+/**
+ * @brief
+ *
+ * @param
+ * @return bool
+ */
+bool nullFilter(T&)
+{
+ return true;
+}
+
+/**
+ * @brief
+ *
+ */
+template <typename T>
+GuessedValue<T>::GuessedValue()
+ : m_Value(), m_Quality(GUESS_INVALID), m_Filter(nullFilter<T>)
+{}
+
+/**
+ * @brief
+ *
+ * @param reference
+ * @param quality
+ */
+template <typename T>
+GuessedValue<T>::GuessedValue(const T& reference, EGuessQuality quality)
+ : m_Value(reference), m_Variants{reference}, m_Quality(quality),
+ m_Filter(nullFilter<T>)
+{}
+
+/**
+ * @brief
+ *
+ * @param reference
+ * @return GuessedValue<T> &GuessedValue<T>
+ */
+template <typename T>
+GuessedValue<T>& GuessedValue<T>::operator=(const GuessedValue<T>& reference)
+{
+ if (this != &reference) {
+ if (reference.m_Quality >= m_Quality) {
+ m_Value = reference.m_Value;
+ m_Quality = reference.m_Quality;
+ m_Filter = reference.m_Filter;
+ m_Variants = reference.m_Variants;
+ }
+ }
+ return *this;
+}
+
+/**
+ * @brief
+ *
+ * @param filterFunction
+ */
+template <typename T>
+void GuessedValue<T>::setFilter(std::function<bool(T&)> filterFunction)
+{
+ m_Filter = filterFunction;
+}
+
+/**
+ * @brief
+ *
+ * @param value
+ * @return GuessedValue<T> &GuessedValue<T>
+ */
+template <typename T>
+GuessedValue<T>& GuessedValue<T>::update(const T& value)
+{
+ T temp = value;
+ if (m_Filter(temp)) {
+ m_Variants.insert(temp);
+ m_Value = temp;
+ }
+ return *this;
+}
+
+/**
+ * @brief
+ *
+ * @param value
+ * @param quality
+ * @return GuessedValue<T> &GuessedValue<T>
+ */
+template <typename T>
+GuessedValue<T>& GuessedValue<T>::update(const T& value, EGuessQuality quality)
+{
+ T temp = value;
+ if (m_Filter(temp)) {
+ m_Variants.insert(temp);
+ if (quality >= m_Quality) {
+ m_Value = temp;
+ m_Quality = quality;
+ }
+ }
+ return *this;
+}
+
+} // namespace MOBase
+
+#endif // GUESSEDVALUE_H
diff --git a/libs/uibase/include/uibase/idownloadmanager.h b/libs/uibase/include/uibase/idownloadmanager.h
new file mode 100644
index 0000000..f22dd0e
--- /dev/null
+++ b/libs/uibase/include/uibase/idownloadmanager.h
@@ -0,0 +1,102 @@
+#ifndef IDOWNLOADMANAGER_H
+#define IDOWNLOADMANAGER_H
+
+#include "dllimport.h"
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <functional>
+
+namespace MOBase
+{
+
+class QDLLEXPORT IDownloadManager : public QObject
+{
+ Q_OBJECT
+
+public:
+ IDownloadManager(QObject* parent = nullptr) : QObject(parent) {}
+
+ /**
+ * @brief download a file by url. The list can contain alternative URLs to allow the
+ * download manager to switch in case of download problems
+ * @param urls list of urls to download from
+ * @return an id by which the download will be identified
+ */
+ virtual int startDownloadURLs(const QStringList& urls) = 0;
+
+ /**
+ * @brief download a file from www.nexusmods.com/<game>. <game> is always the game
+ * currently being managed
+ * @param modID id of the mod for which to download a file
+ * @param fileID id of the file to download
+ * @return an id by which the download will be identified
+ */
+ virtual int startDownloadNexusFile(int modID, int fileID) = 0;
+
+ /**
+ * @brief download a file from www.nexusmods.com/<gameName>.
+ * @param gameName 'short' name of the game the mod is for
+ * @param modID id of the mod for which to download a file
+ * @param fileID id of the file to download
+ * @return an id by which the download will be identified
+ */
+ virtual int startDownloadNexusFileForGame(const QString& gameName, int modID,
+ int fileID) = 0;
+
+ /**
+ * @brief get the (absolute) file path of the specified download.
+ * @param id id of the download as returned by the download... functions
+ * @return absoute path to the downloaded file. This file may not yet exist if the
+ * download is incomplete
+ */
+ virtual QString downloadPath(int id) = 0;
+
+ /**
+ * @brief Installs a handler to be called when a download complete.
+ *
+ * @param callback The function to be called when a download complete. The argument is
+ * the download ID.
+ *
+ * @return true if the handler was successfully installed (there is as of now no known
+ * reason this should fail).
+ */
+ virtual bool onDownloadComplete(const std::function<void(int)>& callback) = 0;
+
+ /**
+ * @brief Installs a handler to be called when a download is paused.
+ *
+ * @param callback The function to be called when a download is paused. The argument
+ * is the download ID.
+ *
+ * @return true if the handler was successfully installed (there is as of now no known
+ * reason this should fail).
+ */
+ virtual bool onDownloadPaused(const std::function<void(int)>& callback) = 0;
+
+ /**
+ * @brief Installs a handler to be called when a download fails.
+ *
+ * @param callback The function to be called when a download fails. The argument is
+ * the download ID.
+ *
+ * @return true if the handler was successfully installed (there is as of now no known
+ * reason this should fail).
+ */
+ virtual bool onDownloadFailed(const std::function<void(int)>& callback) = 0;
+
+ /**
+ * @brief Installs a handler to be called when a download is removed.
+ *
+ * @param callback The function to be called when a download is removed. The argument
+ * is the download ID (which is no longer valid).
+ *
+ * @return true if the handler was successfully installed (there is as of now no known
+ * reason this should fail).
+ */
+ virtual bool onDownloadRemoved(const std::function<void(int)>& callback) = 0;
+};
+
+} // namespace MOBase
+
+#endif // IDOWNLOADMANAGER_H
diff --git a/libs/uibase/include/uibase/iexecutable.h b/libs/uibase/include/uibase/iexecutable.h
new file mode 100644
index 0000000..7873b10
--- /dev/null
+++ b/libs/uibase/include/uibase/iexecutable.h
@@ -0,0 +1,84 @@
+/*
+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 IEXECUTABLE_H
+#define IEXECUTABLE_H
+
+#include <QFileInfo>
+#include <QString>
+
+namespace MOBase
+{
+class IExecutable
+{
+public: // Information found in ExecutableInfo
+ /**
+ * @return the title of the executable.
+ */
+ virtual const QString& title() const = 0;
+
+ /**
+ * @return the file info of the executable binary.
+ */
+ virtual const QFileInfo& binaryInfo() const = 0;
+
+ /**
+ * @return the arguments to be passed to the executable.
+ *
+ * @note This API might be changed in the future to return a QStringList instead.
+ */
+ virtual const QString& arguments() const = 0;
+
+ /**
+ * @return the Steam App ID associated with this executable, or an empty string if
+ * there is none.
+ */
+ virtual const QString& steamAppID() const = 0;
+
+ /**
+ * @return the working directory for the executable.
+ */
+ virtual const QString& workingDirectory() const = 0;
+
+public: // Information found in flags
+ /**
+ * @return true if the executable is shown on the toolbar.
+ */
+ virtual bool isShownOnToolbar() const = 0;
+
+ /**
+ * @return true if the executable's application icon is used for desktop shortcuts.
+ */
+ virtual bool usesOwnIcon() const = 0;
+
+ /**
+ * @return true if Mod Organizer should minimize to the system tray while this
+ * executable is running.
+ */
+ virtual bool minimizeToSystemTray() const = 0;
+
+ /**
+ * @return true if this executable is hidden in the user interface.
+ */
+ virtual bool hide() const = 0;
+};
+} // namespace MOBase
+
+#endif // IEXECUTABLE_H
diff --git a/libs/uibase/include/uibase/iexecutableslist.h b/libs/uibase/include/uibase/iexecutableslist.h
new file mode 100644
index 0000000..f8f7a3f
--- /dev/null
+++ b/libs/uibase/include/uibase/iexecutableslist.h
@@ -0,0 +1,75 @@
+/*
+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 IEXECUTABLESLIST_H
+#define IEXECUTABLESLIST_H
+
+#include <QFileInfo>
+#include <QString>
+
+#include <generator>
+
+#include "iexecutable.h"
+
+namespace MOBase
+{
+/**
+ * @brief Interface to the list of executables configured in Mod Organizer.
+ */
+class IExecutablesList
+{
+public:
+ /**
+ * @brief Retrieve all configured executables.
+ *
+ * @return a generator yielding all configured executables.
+ */
+ virtual std::generator<const IExecutable&> executables() const = 0;
+
+ /**
+ * @brief Retrieve an executable by its title.
+ *
+ * @param title Title of the executable to retrieve.
+ *
+ * @return the executable with the specified title, or nullptr if not found.
+ */
+ virtual const IExecutable* getByTitle(const QString& title) const = 0;
+
+ /**
+ * @brief Retrieve an executable by its binary file info.
+ *
+ * @param info File info of the executable binary to retrieve.
+ *
+ * @return the executable with the specified binary, or nullptr if not found.
+ */
+ virtual const IExecutable* getByBinary(const QFileInfo& info) const = 0;
+
+ /**
+ * @brief Check if an executable with the specified title exists.
+ *
+ * @param title Title of the executable to check.
+ *
+ * @return true if an executable with the specified title exists, false otherwise.
+ */
+ virtual bool contains(const QString& title) const = 0;
+};
+} // namespace MOBase
+
+#endif // IEXECUTABLESLIST_H
diff --git a/libs/uibase/include/uibase/ifiletree.h b/libs/uibase/include/uibase/ifiletree.h
new file mode 100644
index 0000000..65bc171
--- /dev/null
+++ b/libs/uibase/include/uibase/ifiletree.h
@@ -0,0 +1,1130 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2020 MO2 Team. 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 IFILETREE_H
+#define IFILETREE_H
+
+#include <atomic>
+#include <generator>
+#include <iterator>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <stdexcept>
+#include <vector>
+#include <version>
+
+#include <QDateTime>
+#include <QFlags>
+#include <QList>
+#include <QString>
+
+#include "dllimport.h"
+#include "utility.h"
+
+/**
+ * This header contains definition for the interface IFileTree and the FileTreeEntry
+ * class.
+ *
+ * The purpose of IFileTree is to expose a file tree in a user-friendly way. The
+ * IFileTree interface represent a "virtual" file tree: the tree may not exists on
+ * the disk or anywhere, it is just an abstract structure. The source of the tree
+ * is irrelevant to the IFileTree user and is an implementation details.
+ *
+ * IFileTree and FileTreeEntry are very intrically linked so it is not possible to
+ * use FileTreeEntry for something else, and the only way to create FileTreeEntry
+ * is through an existing IFileTree.
+ *
+ * IFileTree expose a mutable and a strict non-mutable interfaces based on
+ * const-qualification of methods, but all underlying implementation are obviously
+ * non-const.
+ *
+ * The IFileTree interface is implemented such that creating implementations should
+ * be fairly easy (two short methods to implement). Implementation can override other
+ * methods in order to reflect changes from the tree to the actual source (e.g.,
+ * override the beforeX() methods to actually rename or move file on the disk).
+ *
+ */
+
+namespace MOBase
+{
+
+/**
+ *
+ */
+class IFileTree;
+
+/**
+ * @brief Simple valid C++ comparator for QString that compare them case-insensitive,
+ * mostly useful to compare filenames on Windows.
+ */
+struct FileNameComparator
+{
+
+ /**
+ * @brief The case sensitivity of filenames.
+ */
+ static constexpr auto CaseSensitivity = Qt::CaseInsensitive;
+
+ /**
+ * @brief Compare the two given filenames.
+ *
+ * @param lhs, rhs Filenames to compare.
+ *
+ * @return -1, 0 or 1 if the first one is less, equal or greater than the second one.
+ */
+ static int compare(QString const& lhs, QString const& rhs)
+ {
+ return lhs.compare(rhs, CaseSensitivity);
+ }
+
+ /**
+ *
+ */
+ bool operator()(QString const& a, QString const& b) const
+ {
+ return compare(a, b) < 0;
+ }
+};
+
+/**
+ * @brief Exception thrown when an operation on the tree is not supported by the
+ * implementation or makes no sense (e.g., creation of a file in an archive).
+ */
+struct QDLLEXPORT UnsupportedOperationException : public Exception
+{
+ using Exception::Exception;
+};
+
+/**
+ * @brief Represent an entry in a file tree, either a file or a directory. This class
+ * inherited by IFileTree so that operations on entry are the same for a file or
+ * a directory.
+ *
+ * This class provides convenience methods to query information on the file, like its
+ * name or the its last modification time. It also provides a convenience astree()
+ * method that can be used to retrieve the tree corresponding to its entry in case the
+ * entry represent a directory.
+ *
+ */
+class QDLLEXPORT FileTreeEntry : public std::enable_shared_from_this<FileTreeEntry>
+{
+
+public: // Enums
+ /**
+ * @brief Enumeration of the different file type.
+ *
+ */
+ enum FileType
+ {
+ DIRECTORY = 0b01,
+ FILE = 0b10
+ };
+ Q_DECLARE_FLAGS(FileTypes, FileType);
+
+ constexpr static auto FILE_OR_DIRECTORY = FileTypes{DIRECTORY, FILE};
+
+public: // Deleted operators:
+ FileTreeEntry(FileTreeEntry const&) = delete;
+ FileTreeEntry(FileTreeEntry&&) = delete;
+
+ FileTreeEntry& operator=(FileTreeEntry const&) = delete;
+ FileTreeEntry& operator=(FileTreeEntry&&) = delete;
+
+public: // Methods
+ /**
+ * @brief Check if this entry is a file.
+ *
+ * @return true if this entry is a file, false otherwize.
+ */
+ bool isFile() const { return astree() == nullptr; }
+
+ /**
+ * @brief Check if this entry is a directory.
+ *
+ * @return true if this entry is a directory, false otherwize.
+ */
+ bool isDir() const { return astree() != nullptr; }
+
+ /**
+ * @brief Convert this entry to a tree. This method returns a null pointer
+ * if this entry corresponds to a file.
+ *
+ * @return this entry as a tree, or a null pointer if isDir() is false.
+ */
+ virtual std::shared_ptr<IFileTree> astree() { return nullptr; }
+
+ /**
+ * @brief Convert this entry to a tree. This method returns a null pointer
+ * if this entry corresponds to a file.
+ *
+ * @return this entry as a tree, or a null pointer if isDir() is false.
+ */
+ virtual std::shared_ptr<const IFileTree> astree() const { return nullptr; }
+
+ /**
+ * @brief Retrieve the type of this entry.
+ *
+ * @return the type of this entry.
+ */
+ FileType fileType() const { return isDir() ? DIRECTORY : FILE; }
+
+ /**
+ * @brief Retrieve the name of this entry.
+ *
+ * @return the name of this entry.
+ */
+ QString name() const { return m_Name; }
+
+ /**
+ * @brief Compare the name of this entry against the given string.
+ *
+ * This method only checks the name of the entry, not the full path.
+ *
+ * @param name Name to test.
+ *
+ * @return -1, 0 or 1 depending on the result of the comparison.
+ */
+ int compare(QString name) const { return FileNameComparator::compare(m_Name, name); }
+
+ /**
+ * @brief Retrieve the "last" extension of this entry.
+ *
+ * The "last" extension is everything after the last dot in the file name.
+ *
+ * @return the last extension of this entry, or an empty string if the file has no
+ * extension or is directory.
+ */
+ QString suffix() const;
+
+ /**
+ * @brief Check if this entry has the given suffix.
+ *
+ * @param suffix Suffix of to check.
+ *
+ * @return true if this entry is a file and has the given suffix.
+ */
+ bool hasSuffix(QString suffix) const;
+
+ /**
+ * @brief Check if this entry has one of the given suffixes.
+ *
+ * @param suffixes Suffixes of to check.
+ *
+ * @return true if this entry is a file and has the given suffix.
+ */
+ bool hasSuffix(QStringList suffixes) const;
+
+ /**
+ * @brief Retrieve the path from this entry up to the root of the tree.
+ *
+ * This method propagate up the tree so is not constant complexity as
+ * the full path is never stored.
+ *
+ * @param sep The type of separator to use to create the path.
+ *
+ * @return the path from this entry to the root, including the name
+ * of this entry.
+ */
+ QString path(QString sep = "\\") const { return pathFrom(nullptr, sep); }
+
+ /**
+ * @brief Retrieve the path from this entry to the given tree.
+ *
+ * @param tree The tree to reach, must be a parent of this entry.
+ * @param sep The type of separator to use to create the path.
+ *
+ * @return the path from this entry up to the given tree, including the name
+ * of this entry, or QString() if the given tree is not a parent of
+ * this one.
+ */
+ QString pathFrom(std::shared_ptr<const IFileTree> tree, QString sep = "\\") const;
+
+ /**
+ * @brief Detach this entry from its parent tree.
+ *
+ * @return true if the entry was removed correctly, false otherwize.
+ */
+ bool detach();
+
+ /**
+ * @brief Move this entry to the given tree.
+ *
+ * @param tree The tree to move this entry to.
+ *
+ * @return true if the entry was moved correctly, false otherwize.
+ */
+ bool moveTo(std::shared_ptr<IFileTree> tree);
+
+ /**
+ * @brief Retrieve the immediate parent tree of this entry.
+ *
+ * @return the parent tree containing this entry, or a null pointer
+ * if this entry is the root or the parent tree is unreachable.
+ */
+ std::shared_ptr<IFileTree> parent()
+ {
+ return std::const_pointer_cast<IFileTree>(
+ const_cast<const FileTreeEntry*>(this)->parent());
+ }
+
+ /**
+ * @brief Retrieve the immediate parent tree of this entry.
+ *
+ * @return the parent tree containing this entry, or a null pointer
+ * if this entry is the root or the parent tree is unreachable.
+ */
+ std::shared_ptr<const IFileTree> parent() const { return m_Parent.lock(); }
+
+public: // Destructor:
+ virtual ~FileTreeEntry() {}
+
+protected: // Constructors:
+ /**
+ * @brief Create a new entry corresponding.
+ *
+ * @param parent The tree containing this entry.
+ * @param name The name of this entry.
+ */
+ FileTreeEntry(std::shared_ptr<const IFileTree> parent, QString name);
+
+ /**
+ * @brief Creates a new orphan entry identical to this entry.
+ *
+ */
+ virtual std::shared_ptr<FileTreeEntry> clone() const;
+
+ /**
+ * @brief Creates a new FileTreeEntry corresponding to a file with the given
+ * parameters.
+ *
+ * The purpose of this methods is to allow child classes corresponding to tree (i.e.,
+ * that do not inherit directly FileTreeEntry) to create FileTreeEntry.
+ *
+ * @param parent The tree containing this file.
+ * @param name The name of this file.
+ */
+ static std::shared_ptr<FileTreeEntry>
+ createFileEntry(std::shared_ptr<const IFileTree> parent, QString name);
+
+private:
+ std::weak_ptr<const IFileTree> m_Parent;
+
+ QString m_Name;
+
+ friend class IFileTree;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(FileTreeEntry::FileTypes);
+
+/**
+ * @brief Interface to classes that provides way to visualize and alter file trees. The
+ * tree may not correspond to an actual file tree on the disk (e.g., inside an archive,
+ * from a QTree Widget, ...).
+ *
+ * This interface already implements most of the usual methods for a file tree. Child
+ * classes only have to implement methods to populate the tree and to create child tree
+ * object.
+ *
+ * Read-only operations on the tree are thread-safe, even when the tree has not been
+ * populated yet.
+ *
+ * In order to prevent wrong usage of the tree, implementing classes may throw
+ * UnsupportedOperationException if an operation is not supported. By default, all
+ * operations are supported, but some may not make sense in many situations.
+ *
+ * The goal of this is not reflect the change made to a IFileTree to the disk, but child
+ * classes may override relevant methods to do so.
+ *
+ * The tree is built upon FileTreeEntry. A given tree holds shared pointers to its
+ * entries while each entry holds a weak pointer to its parent, this means that the
+ * descending link are strong (shared pointers) but the uplink are weaks.
+ *
+ * Accessing the parent is always done by locking the weak pointer so that returned
+ * pointer or either null or valid. This structure implies that as long as the initial
+ * root lives, entry should not be destroyed, unless the entry are detached from the
+ * root and no shared pointers are kept.
+ *
+ * However, it is not guarantee that one can go up the tree from a single node entry. If
+ * the root node is destroyed, it will not be possible to go up the tree, even if we
+ * still have a valid shared pointer.
+ *
+ * The inheritance is made virtual to provide a way for child classes to use both a
+ * custom FileTreeEntry and IFileTree implementations. This has no impact on the usage
+ * of the interface.
+ *
+ */
+class QDLLEXPORT IFileTree : public virtual FileTreeEntry
+{
+public: // Enumerations and aliases:
+ /**
+ *
+ */
+ enum class InsertPolicy
+ {
+ FAIL_IF_EXISTS,
+ REPLACE,
+ MERGE
+ };
+
+ /**
+ * @brief Special constant returns by merge when the merge failed.
+ */
+ constexpr static std::size_t MERGE_FAILED = (std::size_t)-1;
+
+ /**
+ *
+ */
+ using OverwritesType = std::map<std::shared_ptr<const FileTreeEntry>,
+ std::shared_ptr<const FileTreeEntry>>;
+
+public: // Iterators:
+ /**
+ * The standard iterator are constant, but the pointed value are not. Since
+ * we are storing a vector of shared pointer to non-const object, we need this
+ * wrapper to create iterators to shared pointer of const-object to have proper
+ * immutability when IFileTree is const-qualified.
+ *
+ * Note: convert_iterator satisfies std::forward_iterator concept but not
+ * ForwardIterator since dereferencing does not return an lvalue.
+ */
+ template <class U, class V>
+ struct convert_iterator
+ {
+
+ using reference = U;
+ using difference_type = typename V::difference_type;
+ using value_type = U;
+ using pointer = U;
+ using iterator_category = std::forward_iterator_tag;
+
+ friend bool operator==(convert_iterator a, convert_iterator b)
+ {
+ return a.v == b.v;
+ }
+ friend bool operator!=(convert_iterator a, convert_iterator b)
+ {
+ return a.v != b.v;
+ }
+
+ reference operator*() const { return U(*v); }
+ reference operator->() const { return U(*v); }
+
+ convert_iterator& operator++()
+ {
+ v++;
+ return *this;
+ }
+
+ convert_iterator operator++(int)
+ {
+ value_type value = *(*this);
+ (*this)++;
+ return *this;
+ }
+
+ public:
+ convert_iterator() = default;
+ convert_iterator(convert_iterator const&) = default;
+ convert_iterator(convert_iterator&&) = default;
+ convert_iterator& operator=(convert_iterator const&) = default;
+ convert_iterator& operator=(convert_iterator&&) = default;
+
+ protected:
+ V v;
+
+ convert_iterator(V v) : v{v} {}
+ friend class IFileTree;
+ };
+
+ using value_type = std::shared_ptr<FileTreeEntry>;
+ using reference = std::shared_ptr<FileTreeEntry>;
+ using const_reference = std::shared_ptr<const FileTreeEntry>;
+
+ using iterator = std::vector<std::shared_ptr<FileTreeEntry>>::const_iterator;
+ using const_iterator =
+ convert_iterator<std::shared_ptr<const FileTreeEntry>,
+ std::vector<std::shared_ptr<FileTreeEntry>>::const_iterator>;
+
+ using reverse_iterator =
+ std::vector<std::shared_ptr<FileTreeEntry>>::const_reverse_iterator;
+ using const_reverse_iterator = convert_iterator<
+ std::shared_ptr<const FileTreeEntry>,
+ std::vector<std::shared_ptr<FileTreeEntry>>::const_reverse_iterator>;
+
+#if __cplusplus > 201703L
+ static_assert(std::forward_iterator<iterator>);
+ static_assert(std::forward_iterator<const_iterator>);
+ static_assert(std::forward_iterator<reverse_iterator>);
+ static_assert(std::forward_iterator<const_reverse_iterator>);
+#endif
+
+public: // Access methods:
+ /**
+ *
+ */
+ iterator begin() { return {std::cbegin(entries())}; }
+ const_iterator begin() const { return {std::cbegin(entries())}; }
+ const_iterator cbegin() const { return {std::cbegin(entries())}; }
+
+ /**
+ *
+ */
+ reverse_iterator rbegin() { return {std::crbegin(entries())}; }
+ const_reverse_iterator rbegin() const { return {std::crbegin(entries())}; }
+ const_reverse_iterator crbegin() const { return {std::crbegin(entries())}; }
+
+ /**
+ *
+ */
+ iterator end() { return {std::cend(entries())}; }
+ const_iterator end() const { return {std::cend(entries())}; }
+ const_iterator cend() const { return {std::cend(entries())}; }
+
+ /**
+ *
+ */
+ reverse_iterator rend() { return {std::crend(entries())}; }
+ const_reverse_iterator rend() const { return {std::crend(entries())}; }
+ const_reverse_iterator crend() const { return {std::crend(entries())}; }
+
+ /**
+ * @brief Retrieve the number of entries in this tree.
+ *
+ * This is constant if the tree has already been populated.
+ *
+ * @return the number of entries in this tree.
+ */
+ std::size_t size() const { return entries().size(); }
+
+ /**
+ * @brief Retrieve the file entry at the given index.
+ *
+ * @param i Index of the entry to retrieve.
+ *
+ * @return the file entry at the given index.
+ *
+ * @throw std::out_of_range if the index is invalid.
+ */
+ std::shared_ptr<FileTreeEntry> at(std::size_t i)
+ {
+ if (i < size()) {
+ return entries()[i];
+ }
+ throw std::out_of_range("IFileTree::at");
+ }
+ std::shared_ptr<const FileTreeEntry> at(std::size_t i) const
+ {
+ if (i < size()) {
+ return entries()[i];
+ }
+ throw std::out_of_range("IFileTree::at");
+ }
+
+ /**
+ * @brief Check if this tree is empty, i.e., if it contains no entries.
+ *
+ * @return true if the tree is empty, false otherwize.
+ */
+ bool empty() const { return size() == 0; }
+
+ /**
+ * @brief Check if the given entry exists.
+ *
+ * @param path Path to the entry, separated by / or \.
+ * @param type The type of the entry to check.
+ *
+ * @return true if the entry was found, false otherwize.
+ */
+ bool exists(QString path,
+ FileTreeEntry::FileTypes type = FileTreeEntry::FILE_OR_DIRECTORY) const;
+
+ /**
+ * @brief Retrieve the given entry.
+ *
+ * If an entry is found at the given path but does not match the given type,
+ * a null pointer is returned.
+ *
+ * @param path Path to the entry, separated by / or \.
+ * @param type The type of the entry to find.
+ *
+ * @return the entry if found, a null pointer otherwize.
+ */
+ std::shared_ptr<FileTreeEntry> find(QString path, FileTypes type = FILE_OR_DIRECTORY);
+ std::shared_ptr<const FileTreeEntry> find(QString path,
+ FileTypes type = FILE_OR_DIRECTORY) const;
+
+ /**
+ * @brief Convenient method around find() that returns IFileTree instead of entries.
+ *
+ * @param path Path to the directory, separated by / or \.
+ *
+ * @return the directory if found, a null pointer otherwize.
+ */
+ std::shared_ptr<IFileTree> findDirectory(QString path)
+ {
+ auto entry = find(path, DIRECTORY);
+ return (entry != nullptr && entry->isDir()) ? entry->astree() : nullptr;
+ }
+ std::shared_ptr<const IFileTree> findDirectory(QString path) const
+ {
+ auto entry = find(path, DIRECTORY);
+ return (entry != nullptr && entry->isDir()) ? entry->astree() : nullptr;
+ }
+
+ /**
+ * @brief Retrieve the path from this tree to the given entry.
+ *
+ * @param entry The entry to reach, must be in this tree.
+ * @param sep The type of separator to use to create the path.
+ *
+ * @return the path from this tree to the given entry, including the name
+ * of the entry, or QString() if the given entry is not in this tree.
+ */
+ QString pathTo(std::shared_ptr<const FileTreeEntry> entry, QString sep = "\\") const
+ {
+ return entry->pathFrom(astree(), sep);
+ }
+
+public: // Walk & Glob operations
+ enum class WalkReturn
+ {
+
+ /**
+ * @brief Continue walking normally.
+ */
+ CONTINUE,
+
+ /**
+ * @brief Stop walking normally.
+ */
+ STOP,
+
+ /**
+ * @brief Skip this folder (no effect if the entry is a file).
+ */
+ SKIP
+
+ };
+
+ /**
+ * @brief Walk this tree, calling the given function for each entry in it.
+ *
+ * The given callback will be called with two parameters: the path from this tree to
+ * the given entry (with a trailing separator, not including the entry name), and the
+ * actual entry. The method returns a `WalkReturn` object to indicates what to do.
+ *
+ * During the walk, parent tree are guaranteed to be visited before their childrens.
+ * The given function is never called with the current tree.
+ *
+ * @param callback Method to call for each entry in the tree.
+ */
+ void
+ walk(std::function<WalkReturn(QString const&, std::shared_ptr<const FileTreeEntry>)>
+ callback,
+ QString sep = "\\") const;
+
+public: // Utility functions:
+ /**
+ * @brief Create a new orphan empty tree.
+ *
+ * @param name Name of the tree.
+ *
+ * @return a new tree without any parent.
+ */
+ std::shared_ptr<IFileTree> createOrphanTree(QString name = "") const;
+
+public: // Mutable operations:
+ /**
+ * @brief Create a new file directly under this tree.
+ *
+ * This method will return a null pointer if the file already exists and if
+ * replaceIfExists is false. This method invalidates iterators to this tree and
+ * all the subtrees present in the given path.
+ *
+ * @param name Name of the file.
+ * @param replaceIfExists If true and an entry already exists at the given path,
+ * it will be replaced by a new entry. This will replace both files and
+ * directories.
+ *
+ * @return the entry corresponding to the create file, or a null
+ * pointer if the file was not created.
+ */
+ virtual std::shared_ptr<FileTreeEntry> addFile(QString path,
+ bool replaceIfExists = false);
+
+ /**
+ * @brief Create a new directory tree under this tree.
+ *
+ * This method will create missing folders in the given path and will
+ * not fail if the directory already exists but will fail if the given
+ * path contains "." or "..".
+ * This method invalidates iterators to this tree and all the subtrees
+ * present in the given path.
+ *
+ * @param path Path to the directory.
+ *
+ * @return the entry corresponding to the created directory, or a null
+ * pointer if the directory was not created.
+ */
+ virtual std::shared_ptr<IFileTree> addDirectory(QString path);
+
+ /**
+ * @brief Insert the given entry in this tree, removing it from its
+ * previouis parent.
+ *
+ * The entry must not be this tree or a parent entry of this tree.
+ *
+ * - If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry
+ * with the same name already exists.
+ * - If the policy is REPLACE, an existing entry will be replaced by the given entry.
+ * - If MERGE:
+ * - If there is no entry with the same name, the new entry is inserted.
+ * - If there is an entry with the same name:
+ * - If both entries are files, the old file is replaced by the given entry.
+ * - If both entries are directories, a merge is performed as if using merge().
+ * - Otherwize the insertion fails (two entries with different types).
+ *
+ * This method invalidates iterator to this tree, to the parent tree of the given
+ * entry, and to subtrees of this tree if the insert policy is MERGE.
+ *
+ * @param entry Entry to insert.
+ * @param insertPolicy Policy to use on conflict.
+ *
+ * @return an iterator to the inserted tree if it was inserted or if it
+ * already existed, or the end iterator if insertPolicy is FAIL_IF_EXISTS
+ * and an entry with the same name already exists.
+ */
+ iterator insert(std::shared_ptr<FileTreeEntry> entry,
+ InsertPolicy insertPolicy = InsertPolicy::FAIL_IF_EXISTS);
+
+ /**
+ * @brief Merge the given tree with this tree, i.e., insert all entries
+ * of the given tree into this tree.
+ *
+ * The tree must not be this tree or a parent entry of this tree. Files present in
+ * both tree will be replaced by files in the given tree. The overwrites parameter can
+ * be used to track the replaced files. After a merge, the source tree will be
+ * empty but still attached to its parent.
+ *
+ * Note that the merge process makes no distinction between files and directories
+ * when merging: if a directory is present in this tree and a file from source
+ * is in conflict with it, the tree will be removed and the file inserted; if a file
+ * is in this tree and a directory from source is in conflict with it, the file will
+ * be replaced with the directory.
+ *
+ * This method invalidates iterators to this tree, all the subtrees under this tree
+ * present in the given path, and all the subtrees of the given source.
+ *
+ * @param source Tree to merge.
+ * @param overwrites If not null, can be used to create a mapping from
+ * overriden file to new files.
+ *
+ * @return the number of overwritten entries, or MERGE_FAILED if the merge
+ * failed (e.g. because the source is a parent of this tree).
+ */
+ std::size_t merge(std::shared_ptr<IFileTree> source,
+ OverwritesType* overwrites = nullptr);
+
+ /**
+ * @brief Move the given entry to the given path under this tree.
+ *
+ * The entry must not be a parent tree of this tree. This method can also be used
+ * to rename entries.
+ *
+ * If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry
+ * at the same location already exists. If the policy is REPLACE, an existing
+ * entry will be replaced. If MERGE, the entry will be merged with the existing
+ * one (if the entry is a file, and a file exists, the file will be replaced).
+ *
+ * This method invalidates iterator to this tree, to the parent tree of the given
+ * entry, and to subtrees of this tree if the insert policy is MERGE.
+ *
+ * @param entry Entry to insert.
+ * @param path The path to move the entry to. If the path ends with / or \,
+ * the entry will be inserted in the corresponding directory instead of replacing
+ * it. If the given path is empty (`""`), this is equivalent to `insert()`.
+ * @param insertPolicy Policy to use on conflict.
+ *
+ * @return true if the entry was moved correctly, false otherwize.
+ */
+ bool move(std::shared_ptr<FileTreeEntry> entry, QString path = "",
+ InsertPolicy insertPolicy = InsertPolicy::FAIL_IF_EXISTS);
+
+ /**
+ * @brief Copy the given entry to the given path under this tree.
+ *
+ * The entry must not be a parent tree of this tree.
+ *
+ * If the insert policy if FAIL_IF_EXISTS, the call will fail if an entry
+ * at the same location already exists. If the policy is REPLACE, an existing
+ * entry will be replaced. If MERGE, the entry will be merged with the existing
+ * one (if the entry is a file, and a file exists, the file will be replaced).
+ *
+ * This method invalidates iterator to this tree and to subtrees of this tree if
+ * the insert policy is MERGE. The given entry is left untouched
+ *
+ * @param entry Entry to copy.
+ * @param path The path to copy the entry to. If the path ends with / or \,
+ * the entry will be inserted in the corresponding directory instead of replacing
+ * it.
+ * @param insertPolicy Policy to use on conflict.
+ *
+ * @return the copy of the entry if it was copied correctly, a null pointer otherwise.
+ */
+ std::shared_ptr<FileTreeEntry>
+ copy(std::shared_ptr<const FileTreeEntry> entry, QString path = "",
+ InsertPolicy insertPolicy = InsertPolicy::FAIL_IF_EXISTS);
+
+ /**
+ * @brief Delete the given entry.
+ *
+ * @param entry Entry to delete. The entry must belongs to this tree (and
+ * not to a subtree).
+ *
+ * @return an iterator following the removed entry (might be the end
+ * iterator if the entry was not found or was the last).
+ */
+ iterator erase(std::shared_ptr<FileTreeEntry> entry);
+
+ /**
+ * @brief Delete the entry with the given name.
+ *
+ * This method does not recurse into subtrees, so the entry should be
+ * accessible directly from this tree.
+ *
+ * @param name Name of the entry to delete.
+ *
+ * @return a pair containing an iterator following the removed entry (might
+ * be the end iterator if the entry was not found or was the last) and
+ * the removed entry (or a null pointer if the entry was not found).
+ */
+ std::pair<iterator, std::shared_ptr<FileTreeEntry>> erase(QString name);
+
+ /**
+ * @brief Delete (detach) all the entries from this tree
+ *
+ * This method will go through the entries in this tree and stop at the first
+ * entry that cannot be deleted, this means that the tree can be partially cleared.
+ *
+ * @return true if all entries could be deleted, false otherwize.
+ */
+ bool clear();
+
+ /**
+ * @brief Delete the entries with the given names from the tree.
+ *
+ * This method does not recurse into subtrees, so the entry should be
+ * accessible directly from this tree. This method invalidates iterators.
+ *
+ * @param names Names of the entries to delete.
+ *
+ * @return the number of deleted entry.
+ */
+ std::size_t removeAll(QStringList names);
+
+ /**
+ * @brief Delete the entries that match the given predicate from the tree.
+ *
+ * This method does not recurse into subtrees, so the entry should be
+ * accessible directly from this tree. This method invalidates iterators.
+ *
+ * @param predicate Predicate that should return true for entries to delete.
+ *
+ * @return the number of deleted entry.
+ */
+ std::size_t
+ removeIf(std::function<bool(std::shared_ptr<FileTreeEntry> const& entry)> predicate);
+
+public: // Inherited methods:
+ /**
+ * @brief Retrieve the tree corresponding to this entry. Returns a null pointer
+ * if this entry corresponds to a file.
+ *
+ * @return the tree corresponding to this entry, or a null pointer if
+ * isDir() is false.
+ */
+ virtual std::shared_ptr<IFileTree> astree() override
+ {
+ return std::dynamic_pointer_cast<IFileTree>(shared_from_this());
+ }
+
+ /**
+ * @brief Retrieve the tree corresponding to this entry. Returns a null pointer
+ * if this entry corresponds to a file.
+ *
+ * @return the tree corresponding to this entry, or a null pointer if
+ * isDir() is false.
+ */
+ virtual std::shared_ptr<const IFileTree> astree() const override
+ {
+ return std::dynamic_pointer_cast<const IFileTree>(shared_from_this());
+ }
+
+public: // Destructor:
+ virtual ~IFileTree() {}
+
+public: // Deleted operators:
+ IFileTree(IFileTree const&) = delete;
+ IFileTree(IFileTree&&) = delete;
+
+ IFileTree& operator=(IFileTree const&) = delete;
+ IFileTree& operator=(IFileTree&&) = delete;
+
+ /**
+ * A few implementation details here for implementing classes. While there are
+ * multiple virtual public methods, most implementation should not have to
+ * re-implement them.
+ *
+ * There are three pure virtual methods that needs to be implemented by any child
+ * class:
+ * - makeDirectory(): used to create directories - this method serves to create
+ * directory that may or may not existing in the underlying source. Implementing class
+ * do not have to rely on this for `doPopulate()`. This method is also called when new
+ * directory needs to be created (addDirectory, insert, createOrphanTree,
+ * merge, etc.), and may return a null pointer to indicate that the operations failed
+ * or is not permitted.
+ * - doClone(): called when a tree needs to be cloned (e.g., for a copy) - this
+ * methods does not copy the subtrees, it should only create an empty tree equivalent
+ * to the current tree.
+ * - doPopulate(): called when a tree have to be populated.
+ *
+ * The other commons methods that can be re-implemented are:
+ * - makeFile(), this is used to create new file - this is very similar to
+ * makeDirectory() except that it has a default implementation that simply creates a
+ * FileTreeEntry.
+ * - beforeInsert(), beforeReplace() and beforeRemove(): these can be implemented to
+ * 1) prevent some operations, 2) perform operations on the actual tree (e.g., move a
+ * file on the disk).
+ */
+protected:
+ friend class FileTreeEntry;
+
+ /**
+ * Split the given path into parts.
+ *
+ * @param path The path to split.
+ *
+ * @return a list containing the section of the path.
+ */
+ static QStringList splitPath(QString path);
+
+ /**
+ * @brief Called before replacing an entry with another one.
+ *
+ * This is a pre-method meaning that it can be used to prevent an operation on
+ * the tree.
+ *
+ * This method is for internal usage only and is called when an entry is going to
+ * be replaced by another (because there is name conflict).
+ *
+ * The base implementation of this method does nothing (the actual replacement is
+ * made elsewhere). This method can be used to prevent a replacement by returning
+ * false.
+ *
+ * @param dstTree Tree containing the destination entry.
+ * @param destination Entry that will be replaced.
+ * @param source Entry that will replace the destination.
+ *
+ * @return true if the entry can be replaced, false otherwize.
+ */
+ virtual bool beforeReplace(IFileTree const* dstTree, FileTreeEntry const* destination,
+ FileTreeEntry const* source);
+
+ /**
+ * @brief Called before inserting an entry in a tree.
+ *
+ * This is a pre-method meaning that it can be used to prevent an operation on
+ * the tree.
+ *
+ * This method is for internal usage only and is called when an entry is going to
+ * be inserted in a tree. This method is not called after makeFile() or
+ * makeDirectory() so those should be used to prevent creation of files and
+ * directories.
+ *
+ * The base implementation of this method does nothing (the actual insertion is
+ * made elsewhere). This method can be used to prevent an insertion by returning
+ * false.
+ *
+ * @param tree Tree into which the entry will be inserted.
+ * @param source Entry that will be inserted the destination.
+ *
+ * @return true if the entry can be inserted, false otherwize.
+ */
+ virtual bool beforeInsert(IFileTree const* entry, FileTreeEntry const* name);
+
+ /**
+ * @brief Called before removing an entry.
+ *
+ * This is a pre-method meaning that it can be used to prevent an operation on
+ * the tree.
+ *
+ * This method is for internal usage only and is called when an entry is going to
+ * be removed from a tree.
+ *
+ * The base implementation of this method does nothing (the actual removal is
+ * made elsewhere). This method can be used to prevent it by returning false.
+ *
+ * @param tree Tree containing the entry.
+ * @param entry Entry that will be removed.
+ *
+ * @return true if the entry can be removed, false otherwize.
+ */
+ virtual bool beforeRemove(IFileTree const* entry, FileTreeEntry const* name);
+
+ /**
+ * @brief Create a new file under this tree.
+ *
+ * @param parent The current tree, without const-qualification.
+ * @param name Name of the file.
+ *
+ * @return the created file.
+ */
+ virtual std::shared_ptr<FileTreeEntry>
+ makeFile(std::shared_ptr<const IFileTree> parent, QString name) const;
+
+ /**
+ * @brief Create a new entry corresponding to a subtree under this tree.
+ *
+ * @param parent The current tree, without const-qualification.
+ * @param name Name of the directory.
+ *
+ * @return the entry for the created directory.
+ */
+ virtual std::shared_ptr<IFileTree>
+ makeDirectory(std::shared_ptr<const IFileTree> parent, QString name) const = 0;
+
+ /**
+ * @brief Method that child classes should implement.
+ *
+ * This method should populate the given entries of this tree by using the makeFile
+ * and makeTree method. The usage of the makeFile and makeTree method is not mandatory
+ * here.
+ *
+ * If the implementation can populate the vector of entries in order, it is possible
+ * to return false to tell IFileTree not to re-sort the vector. If sorted, directories
+ * should be before files, and both directories and files should be sorted by name in
+ * a case-insensitive way.
+ *
+ * @param parent The current tree, without const-qualification.
+ * @param entries Vector of entries to populate.
+ *
+ * @return true if the vector of entries is already sorted, false if it must be
+ * sorted.
+ */
+ virtual bool
+ doPopulate(std::shared_ptr<const IFileTree> parent,
+ std::vector<std::shared_ptr<FileTreeEntry>>& entries) const = 0;
+
+ /**
+ * @brief Creates a copy of this file tree.
+ *
+ * This methods is called by clone() in order to copy child class attributes. This
+ * method should basically returns a new copy of the tree with the attributes added by
+ * the implementation copied (and nothing else).
+ *
+ * @return the cloned tree.
+ */
+ virtual std::shared_ptr<IFileTree> doClone() const = 0;
+
+protected: // Constructor
+ /**
+ * @brief Creates a new tree. This method takes no parameter since, due to the virtual
+ * inheritance, child classes must directly call the FileTreeEntry constructor.
+ */
+ IFileTree();
+
+ /**
+ *
+ */
+ std::shared_ptr<FileTreeEntry> clone() const override;
+
+ /**
+ *
+ */
+private: // Private stuff, look away!
+ /**
+ * @brief Retrieve the entry corresponding to the given path.
+ *
+ * @param path Path to entry.
+ * @param matchType Type of file to check.
+ *
+ * @return the entry, or a null pointer if the entry did not exist.
+ */
+ std::shared_ptr<FileTreeEntry> fetchEntry(QStringList path, FileTypes matchType);
+ std::shared_ptr<const FileTreeEntry> fetchEntry(QStringList const& path,
+ FileTypes matchType) const;
+
+ /**
+ * @brief Merge the source tree into the destination tree. On conflict, the source
+ * entries are always chosen.
+ *
+ * @param destination Destination tree.
+ * @param source Source tree.
+ *
+ * @return the number of overwritten entries.
+ */
+ std::size_t mergeTree(std::shared_ptr<IFileTree> destination,
+ std::shared_ptr<IFileTree> source, OverwritesType* overwrites);
+
+ /**
+ * @brief Create a new subtree under the given tree.
+ *
+ * This method will create missing folders in the given path and will not fail if the
+ * directory already exists but will fail the given path contains "." or "..".
+ *
+ * @param begin, end Range of section of the path.
+ *
+ * @return the entry corresponding to the create tree, or a null pointer if the tree
+ * was not created.
+ */
+ std::shared_ptr<IFileTree> createTree(QStringList::const_iterator begin,
+ QStringList::const_iterator end);
+
+ // Indicate if this tree has been populated:
+ mutable std::atomic<bool> m_Populated{false};
+ mutable std::once_flag m_OnceFlag;
+ mutable std::vector<std::shared_ptr<FileTreeEntry>> m_Entries;
+
+ /**
+ * @brief Retrieve the vector of entries after populating it if required.
+ *
+ * @return the vector of entries.
+ */
+ std::vector<std::shared_ptr<FileTreeEntry>>& entries();
+ const std::vector<std::shared_ptr<FileTreeEntry>>& entries() const;
+
+ /**
+ * @brief Populate the internal vectors and update the flag.
+ */
+ void populate() const;
+};
+
+} // namespace MOBase
+
+// __has_cpp_attribute(__cpp_lib_generator) does not seem to work, maybe some conflict
+// with Qt?
+#ifdef __cpp_lib_generator
+
+#include "ifiletree_utils.h"
+
+#endif
+
+#endif
diff --git a/libs/uibase/include/uibase/ifiletree_utils.h b/libs/uibase/include/uibase/ifiletree_utils.h
new file mode 100644
index 0000000..9340788
--- /dev/null
+++ b/libs/uibase/include/uibase/ifiletree_utils.h
@@ -0,0 +1,82 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2020 MO2 Team. 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 UIBASE_IFILETREE_UTILS_H
+#define UIBASE_IFILETREE_UTILS_H
+
+#include <generator>
+
+#include <QString>
+
+#include "dllimport.h"
+#include "exceptions.h"
+#include "ifiletree.h"
+
+namespace MOBase
+{
+
+/**
+ * @brief Exception thrown when an invalid glob pattern is specified.
+ */
+struct QDLLEXPORT InvalidGlobPatternException : public Exception
+{
+ using Exception::Exception;
+};
+
+enum class GlobPatternType
+{
+ /**
+ * @brief Glob mode, similar to python pathlib.Path.glob function
+ */
+ GLOB,
+
+ /**
+ * @brief Regex mode, each part of the pattern (between / or \) is considered a
+ * regex, except for ** which is still considered as glob.
+ */
+ REGEX
+};
+
+/**
+ * @brief Walk this tree, returning entries.
+ *
+ * During the walk, parent tree are guaranteed to be visited before their childrens.
+ * The current tree is not included in the return generator.
+ *
+ * @return a generator over the entries.
+ */
+QDLLEXPORT std::generator<std::shared_ptr<const FileTreeEntry>>
+walk(std::shared_ptr<const IFileTree> fileTree);
+
+/**
+ * @brief Glob entries matching the given pattern in this tree.
+ *
+ * @param pattern Glob pattern to match, using the same syntax as QRegularExpression.
+ * @param patternType Type of the pattern.
+ *
+ * @return a generator over the entries matching the given pattern.
+ */
+QDLLEXPORT std::generator<std::shared_ptr<const FileTreeEntry>>
+glob(std::shared_ptr<const IFileTree> fileTree, QString pattern,
+ GlobPatternType patternType = GlobPatternType::GLOB);
+
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/iinstallationmanager.h b/libs/uibase/include/uibase/iinstallationmanager.h
new file mode 100644
index 0000000..ef02b29
--- /dev/null
+++ b/libs/uibase/include/uibase/iinstallationmanager.h
@@ -0,0 +1,128 @@
+/*
+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 IINSTALLATIONMANAGER_H
+#define IINSTALLATIONMANAGER_H
+
+#include <QList>
+#include <QString>
+
+#include "ifiletree.h"
+#include "iplugininstaller.h"
+
+namespace MOBase
+{
+
+template <typename T>
+class GuessedValue;
+
+/**
+ * @brief The IInstallationManager class.
+ */
+class IInstallationManager
+{
+
+public:
+ virtual ~IInstallationManager() {}
+
+ /**
+ * @return the extensions of archives supported by this installation manager.
+ */
+ virtual QStringList getSupportedExtensions() const = 0;
+
+ /**
+ * @brief Extract the specified file from the currently opened archive to a temporary
+ * location.
+ *
+ * This method cannot be used to extract directory.
+ *
+ * @param entry Entry corresponding to the file to extract.
+ * @param silent If true, the dialog showing extraction progress will not be shown.
+ *
+ * @return the absolute path to the temporary file, or an empty string if the
+ * file was not extracted.
+ *
+ * @note The call will fail with an exception if no archive is open (plugins deriving
+ * from IPluginInstallerSimple can rely on that, custom installers should not).
+ * @note The temporary file is automatically cleaned up after the installation.
+ * @note This call can be very slow if the archive is large and "solid".
+ */
+ virtual QString extractFile(std::shared_ptr<const FileTreeEntry> entry,
+ bool silent = false) = 0;
+
+ /**
+ * @brief Extract the specified files from the currently opened archive to a temporary
+ * location.
+ *
+ * @param entres Entries corresponding to the files to extract.
+ * @param silent If true, the dialog showing extraction progress will not be shown.
+ *
+ * This method cannot be used to extract directory.
+ *
+ * @return the list of absolute paths to the temporary files.
+ *
+ * @note The call will fail with an exception if no archive is open (plugins deriving
+ * from IPluginInstallerSimple can rely on that, custom installers should not).
+ * @note The temporary file is automatically cleaned up after the installation.
+ * @note This call can be very slow if the archive is large and "solid".
+ *
+ * The flatten argument is not present here while it is present in the deprecated
+ * QStringList version for multiple reasons: 1) it was never used, 2) it is kind of
+ * fishy because there is no way to know if a file is going to be overriden, 3) it is
+ * quite easy to flatten a IFileTree and thus to given a list of entries flattened
+ * (this was not possible with the QStringList version since these were based on the
+ * name of the file inside the archive).
+ */
+ virtual QStringList
+ extractFiles(std::vector<std::shared_ptr<const FileTreeEntry>> const& entries,
+ bool silent = false) = 0;
+
+ /**
+ * @brief Create a new file on the disk corresponding to the given entry.
+ *
+ * This method can be used by installer that needs to create files that are not in the
+ * original archive. At the end of the installation, if there are entries in the final
+ * tree that were used to create files, the corresponding files will be moved to the
+ * mod folder.
+ *
+ * @param entry The entry for which a temporary file should be created.
+ *
+ * @return the path to the created file, or an empty QString() if the file could not
+ * be created.
+ */
+ virtual QString createFile(std::shared_ptr<const MOBase::FileTreeEntry> entry) = 0;
+
+ /**
+ * @brief Installs the given archive.
+ *
+ * @param modName Suggested name of the mod.
+ * @param archiveFile Path to the archive to install.
+ * @param modId ID of the mod, if available.
+ *
+ * @return the installation result.
+ */
+ virtual IPluginInstaller::EInstallResult
+ installArchive(MOBase::GuessedValue<QString>& modName, const QString& archiveFile,
+ int modID = 0) = 0;
+};
+
+} // namespace MOBase
+
+#endif // IINSTALLATIONMANAGER_H
diff --git a/libs/uibase/include/uibase/iinstance.h b/libs/uibase/include/uibase/iinstance.h
new file mode 100644
index 0000000..1c59d4a
--- /dev/null
+++ b/libs/uibase/include/uibase/iinstance.h
@@ -0,0 +1,61 @@
+/*
+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 IINSTANCE_H
+#define IINSTANCE_H
+
+#include <QString>
+
+namespace MOBase
+{
+class IPluginGame;
+
+/**
+ * @brief Represents a Mod Organizer instance, either global or portable.
+ */
+class IInstance
+{
+public:
+ /**
+ * @return The instance name; this is the directory name or "Portable" for portable
+ * instances.
+ */
+ virtual QString displayName() const = 0;
+
+ /**
+ * @return The name of the game managed by this instance, or an empty string if the
+ * INI file could not be read.
+ */
+ virtual QString gameName() const = 0;
+
+ /**
+ * @return The directory where the game is installed, or an empty string if the INI
+ * file could not be read.
+ */
+ virtual QString gameDirectory() const = 0;
+
+ /**
+ * @return true if this is a portable instance, false if it is a global one.
+ */
+ virtual bool isPortable() const = 0;
+};
+} // namespace MOBase
+
+#endif // IINSTANCE_H
diff --git a/libs/uibase/include/uibase/iinstancemanager.h b/libs/uibase/include/uibase/iinstancemanager.h
new file mode 100644
index 0000000..c57479a
--- /dev/null
+++ b/libs/uibase/include/uibase/iinstancemanager.h
@@ -0,0 +1,66 @@
+/*
+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 IINSTANCEMANAGER_H
+#define IINSTANCEMANAGER_H
+
+#include <QDir>
+#include <QString>
+
+#include <memory>
+#include <vector>
+
+namespace MOBase
+{
+class IInstance;
+
+/**
+ * @brief Interface to the instance manager of Mod Organizer.
+ */
+class IInstanceManager
+{
+public:
+ /**
+ * @return the current instance.
+ */
+ virtual std::shared_ptr<IInstance> currentInstance() const = 0;
+
+ /**
+ * @return The list of absolute paths to all global instances.
+ *
+ * @note This does not include portable instances.
+ */
+ virtual std::vector<QDir> globalInstancePaths() const = 0;
+
+ /**
+ * @brief Retrieve the global instance corresponding to the given name.
+ *
+ * @param instanceName Name of the global instance to retrieve. This is the directory
+ * name of the instance.
+ *
+ * @return the global instance corresponding to the given name, or nullptr if no such
+ * instance exists.
+ */
+ virtual std::shared_ptr<const IInstance>
+ getGlobalInstance(const QString& instanceName) const = 0;
+};
+} // namespace MOBase
+
+#endif // IINSTANCEMANAGER_H
diff --git a/libs/uibase/include/uibase/imodinterface.h b/libs/uibase/include/uibase/imodinterface.h
new file mode 100644
index 0000000..a2684b4
--- /dev/null
+++ b/libs/uibase/include/uibase/imodinterface.h
@@ -0,0 +1,343 @@
+/*
+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 IMODINTERFACE_H
+#define IMODINTERFACE_H
+
+#include <memory>
+#include <set>
+#include <utility>
+
+#include <QColor>
+#include <QDateTime>
+#include <QList>
+#include <QString>
+
+namespace MOBase
+{
+
+class VersionInfo;
+class IFileTree;
+
+enum class EndorsedState
+{
+ ENDORSED_FALSE,
+ ENDORSED_TRUE,
+ ENDORSED_UNKNOWN,
+ ENDORSED_NEVER
+};
+
+enum class TrackedState
+{
+ TRACKED_FALSE,
+ TRACKED_TRUE,
+ TRACKED_UNKNOWN,
+};
+
+class IModInterface
+{
+public:
+ virtual ~IModInterface() {}
+
+public: // Non-meta related information:
+ /**
+ * @return the name of the mod.
+ */
+ virtual QString name() const = 0;
+
+ /**
+ * @return the absolute path to the mod to be used in file system operations.
+ */
+ virtual QString absolutePath() const = 0;
+
+public: // Meta-related information:
+ /**
+ * @return the comments for this mod, if any.
+ */
+ virtual QString comments() const = 0;
+
+ /**
+ * @return the notes for this mod, if any.
+ */
+ virtual QString notes() const = 0;
+
+ /**
+ * @brief Retrieve the short name of the game associated with this mod. This may
+ * differ from the current game plugin (e.g. you can install a Skyrim LE game in a SSE
+ * installation).
+ *
+ * @return the name of the game associated with this mod.
+ */
+ virtual QString gameName() const = 0;
+
+ /**
+ * @return the name of the repository from which this mod was installed.
+ */
+ virtual QString repository() const = 0;
+
+ /**
+ * @return the Nexus ID of this mod.
+ */
+ virtual int nexusId() const = 0;
+
+ /**
+ * @return the current version of this mod.
+ */
+ virtual VersionInfo version() const = 0;
+
+ /**
+ * @return the newest version of thid mod (as known by MO2). If this matches
+ * version(), then the mod is up-to-date.
+ */
+ virtual VersionInfo newestVersion() const = 0;
+
+ /**
+ * @return the ignored version of this mod (for update), or an invalid version if the
+ * user did not ignore version for this mod.
+ */
+ virtual VersionInfo ignoredVersion() const = 0;
+
+ /**
+ * @return the absolute path to the file that was used to install this mod.
+ */
+ virtual QString installationFile() const = 0;
+
+ virtual std::set<std::pair<int, int>> installedFiles() const = 0;
+
+ /**
+ * @return true if this mod was marked as converted by the user.
+ *
+ * @note When a mod is for a different game, a flag is shown to users to warn them,
+ * but they can mark mods as converted to remove this flag.
+ */
+ virtual bool converted() const = 0;
+
+ /**
+ * @return true if th is mod was marked as containing valid game data.
+ *
+ * @note MO2 uses ModDataChecker to check the content of mods, but sometimes these
+ * fail, in which case mods are incorrectly marked as 'not containing valid games
+ * data'. Users can choose to mark these mods as valid to hide the warning / flag.
+ */
+ virtual bool validated() const = 0;
+
+ /**
+ * @return the color of the 'Notes' column chosen by the user.
+ */
+ virtual QColor color() const = 0;
+
+ /**
+ * @return the URL of this mod, or an empty QString() if no URL is associated
+ * with this mod.
+ */
+ virtual QString url() const = 0;
+
+ /**
+ * @return the ID of the primary category of this mod.
+ */
+ virtual int primaryCategory() const = 0;
+
+ /**
+ * @return the list of categories this mod belongs to.
+ */
+ virtual QStringList categories() const = 0;
+
+ /**
+ * @return the mod author.
+ */
+ virtual QString author() const = 0;
+
+ /**
+ * @return the mod uploader.
+ */
+ virtual QString uploader() const = 0;
+
+ /**
+ * @return the URL of the uploader.
+ */
+ virtual QString uploaderUrl() const = 0;
+
+ /**
+ * @return the tracked state of this mod.
+ */
+ virtual TrackedState trackedState() const = 0;
+
+ /**
+ * @return the endorsement state of this mod.
+ */
+ virtual EndorsedState endorsedState() const = 0;
+
+ /**
+ * @brief Retrieve a file tree corresponding to the underlying disk content
+ * of this mod.
+ *
+ * The file tree should not be cached since it is already cached and updated when
+ * required.
+ *
+ * @return a file tree representing the content of this mod.
+ */
+ virtual std::shared_ptr<const MOBase::IFileTree> fileTree() const = 0;
+
+ /**
+ * @return true if this object represents the overwrite mod.
+ */
+ virtual bool isOverwrite() const = 0;
+
+ /**
+ * @return true if this object represents a backup.
+ */
+ virtual bool isBackup() const = 0;
+
+ /**
+ * @return true if this object represents a separator.
+ */
+ virtual bool isSeparator() const = 0;
+
+ /**
+ * @return true if this object represents a foreign mod.
+ */
+ virtual bool isForeign() const = 0;
+
+public: // Mutable operations:
+ /**
+ * @brief set/change the version of this mod
+ * @param version new version of the mod
+ */
+ virtual void setVersion(const VersionInfo& version) = 0;
+
+ /**
+ * @brief sets the installation file for this mod
+ * @param fileName archive file name
+ */
+ virtual void setInstallationFile(const QString& fileName) = 0;
+
+ /**
+ * @brief set/change the latest known version of this mod
+ * @param version newest known version of the mod
+ */
+ virtual void setNewestVersion(const VersionInfo& version) = 0;
+
+ /**
+ * @brief set endorsement state of the mod
+ * @param endorsed new endorsement state
+ */
+ virtual void setIsEndorsed(bool endorsed) = 0;
+
+ /**
+ * @brief sets the mod id on nexus for this mod
+ * @param the new id to set
+ */
+ virtual void setNexusID(int nexusID) = 0;
+
+ /**
+ * @brief sets the category id from a nexus category id. Conversion to MO id happens
+ * internally
+ * @param categoryID the nexus category id
+ * @note if a mapping is not possible, the category is set to the default value
+ */
+ virtual void addNexusCategory(int categoryID) = 0;
+
+ /**
+ * @brief assign a category to the mod. If the named category doesn't exist it is
+ * created
+ * @param categoryName name of the new category
+ */
+ virtual void addCategory(const QString& categoryName) = 0;
+
+ /**
+ * @brief unassign a category from this mod.
+ * @param categoryName name of the category to be removed
+ * @return true if the category was removed successfully, false if no such category
+ * was assigned
+ */
+ virtual bool removeCategory(const QString& categoryName) = 0;
+
+ /**
+ * @brief set/change the source game of this mod
+ *
+ * @param gameName the source game shortName
+ */
+ virtual void setGameName(const QString& gameName) = 0;
+
+ /**
+ * @brief Set a URL for this mod.
+ *
+ * @param url The URL of this mod.
+ */
+ 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.
+ */
+ virtual QVariant pluginSetting(const QString& pluginName, const QString& key,
+ const QVariant& defaultValue = QVariant()) const = 0;
+
+ /**
+ * @brief Retrieve the settings in this mod for a plugin.
+ *
+ * @param pluginName Name of the plugin for which to retrieve settings. This should
+ * always be IPlugin::name() unless you have a really good reason to access settings
+ * of another plugin.
+ *
+ * @return a map from setting key to value. The map is empty if there are not settings
+ * for this mod.
+ */
+ virtual std::map<QString, QVariant>
+ pluginSettings(const QString& pluginName) const = 0;
+
+ /**
+ * @brief Set 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 value New value for the setting to set.
+ *
+ * @return true if the setting was set correctly, false otherwise.
+ */
+ virtual bool setPluginSetting(const QString& pluginName, const QString& key,
+ const QVariant& value) = 0;
+
+ /**
+ * @brief Remove all the settings of the specified plugin from th is mod.
+ *
+ * @param pluginName Name of the plugin for which settings should be removed. This
+ * should always be IPlugin::name() unless you have a really good reason to access
+ * settings of another plugin.
+ *
+ * @return the old settings from the given plugin, as returned by `pluginSettings()`.
+ */
+ virtual std::map<QString, QVariant>
+ clearPluginSettings(const QString& pluginName) = 0;
+};
+
+} // namespace MOBase
+
+#endif // IMODINTERFACE_H
diff --git a/libs/uibase/include/uibase/imodlist.h b/libs/uibase/include/uibase/imodlist.h
new file mode 100644
index 0000000..7f7a2a1
--- /dev/null
+++ b/libs/uibase/include/uibase/imodlist.h
@@ -0,0 +1,229 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2013 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 IMODLIST_H
+#define IMODLIST_H
+
+#include <functional>
+#include <map>
+
+#include <QList>
+#include <QString>
+
+#include "imodinterface.h"
+#include "iprofile.h"
+
+namespace MOBase
+{
+
+/**
+ * @brief interface to the mod-list
+ * @note all api functions in this interface work need the internal name of a mod to
+ * find a mod. For regular mods (mods the user installed) the display name (as shown to
+ * the user) and internal name are identical. For other mods (non-MO mods) there is
+ * currently no way to translate from display name to internal name because the display
+ * name might not me un-ambiguous.
+ */
+class IModList
+{
+
+public:
+ enum ModState
+ {
+ STATE_EXISTS = 0x00000001,
+ STATE_ACTIVE = 0x00000002,
+ STATE_ESSENTIAL = 0x00000004,
+ STATE_EMPTY = 0x00000008,
+ STATE_ENDORSED = 0x00000010,
+ STATE_VALID = 0x00000020,
+ STATE_ALTERNATE = 0x00000040
+ };
+
+ Q_DECLARE_FLAGS(ModStates, ModState)
+
+public:
+ virtual ~IModList() {}
+
+ /**
+ * @brief retrieves the display name of a mod from it's internal name
+ * @param internalName the internal name
+ * @return a string intended to identify the name to the user
+ * @note If you received an internal name from an api (i.e. IPluginList::origin) then
+ * you should use that name to identify the mod to all other api calls but use this
+ * function to retrieve the name to show to the user.
+ */
+ virtual QString displayName(const QString& internalName) const = 0;
+
+ /**
+ * @brief Retrieve a list of all installed mod names.
+ *
+ * @return list of mods (internal names).
+ */
+ virtual QStringList allMods() const = 0;
+
+ /**
+ * @brief Retrieve the list of installed mod names, sorted by current profile
+ * priority.
+ *
+ * @param profile The profile to use for the priority. If nullptr, the current
+ * profile is used.
+ *
+ * @return list of mods (internal names), sorted by priority.
+ */
+ virtual QStringList
+ allModsByProfilePriority(MOBase::IProfile* profile = nullptr) const = 0;
+
+ /**
+ * @brief Retrieve the mod with the given name.
+ *
+ * @param name Name of the mod to retrieve.
+ *
+ * @return the mod with the given name, or a null pointer if there is no mod with this
+ * name.
+ */
+ virtual IModInterface* getMod(const QString& name) const = 0;
+
+ /**
+ * @brief Remove a mod (from disc and from the ui).
+ *
+ * @param mod The mod to remove.
+ *
+ * @return true on success, false on error.
+ */
+ virtual bool removeMod(MOBase::IModInterface* mod) = 0;
+
+ /**
+ * @brief Rename the given mod.
+ *
+ * This invalidate the mod so you should use the returned value afterwards.
+ *
+ * @param mod The mod to rename.
+ *
+ * @return the new mod (after renaming) on success, a null pointer on error.
+ */
+ virtual MOBase::IModInterface* renameMod(MOBase::IModInterface* mod,
+ const QString& name) = 0;
+
+ /**
+ * @brief retrieve the state of a mod
+ * @param name name of the mod
+ * @return a bitset of information about the mod
+ */
+ virtual ModStates state(const QString& name) const = 0;
+
+ /**
+ * @brief enable or disable a mod
+ *
+ * @param name name of the mod
+ * @param active if true the mod is enabled, otherwise it's disabled
+ *
+ * @return true on success, false if the mod name is not valid
+ *
+ * @note calling this will cause MO to re-evaluate its virtual file system so this is
+ * fairly expensive
+ */
+ virtual bool setActive(const QString& name, bool active) = 0;
+
+ /**
+ * @brief enable or disable a list of mod
+ *
+ * @param names names of the mod
+ * @param active if true mods are enabled, otherwise they are disabled
+ *
+ * @return the number of mods successfully enabled or disabled
+ *
+ * @note calling this will cause MO to re-evaluate its virtual file system so this is
+ * fairly expensive
+ */
+ virtual int setActive(const QStringList& names, bool active) = 0;
+
+ /**
+ * @brief retrieve the priority of a mod
+ * @param name name of the mod
+ * @return the priority (the higher the more important). Returns -1 if the mod doesn't
+ * exist
+ */
+ virtual int priority(const QString& name) const = 0;
+
+ /**
+ * @brief change the priority of a mod
+ * @param name name of the mod
+ * @param newPriority new priority of the mod
+ * @return true on success, false if the priority change was not possible. This is
+ * usually because one of the parameters is invalid
+ * @note Very important: newPriority is the new priority after the move. Keep in mind
+ * that the mod disappears from it's old location and all mods with higher priority
+ * than the moved mod decrease in priority by one.
+ */
+ virtual bool setPriority(const QString& name, int newPriority) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a new mod is installed.
+ *
+ * Parameters of the callback:
+ * - The installed mod.
+ *
+ * @param func Function to called when a mod has been installed.
+ */
+ virtual bool onModInstalled(const std::function<void(IModInterface*)>& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a mod is removed.
+ *
+ * Parameters of the callback:
+ * - The name of the removed mod.
+ *
+ * @param func Function to called when a mod has been removed.
+ */
+ virtual bool onModRemoved(const std::function<void(QString const&)>& func) = 0;
+
+ /**
+ * @brief Installs a handler for the event that the state of mods changed
+ * (enabled/disabled, endorsed, ...).
+ *
+ * Parameters of the callback:
+ * - Map containing the mods whose states have changed. Keys are mod names and
+ * values are mod states.
+ *
+ * @param func The signal to be called when the state of any mod changes.
+ *
+ * @return true if the handler was successfully installed, false otherwise (there is
+ * as of now no known reason this should fail).
+ */
+ virtual bool onModStateChanged(
+ const std::function<void(const std::map<QString, ModStates>&)>& func) = 0;
+
+ /**
+ * installs a handler for the event that a mod changes priority
+ * @param func the signal to be called when the priority of a mod changes
+ * (first parameter for the handler is the name of the mod, second is the old
+ * priority, third the new one)
+ * @return true if the handler was successfully installed (there is as of now no known
+ * reason this should fail)
+ */
+ virtual bool
+ onModMoved(const std::function<void(const QString&, int, int)>& func) = 0;
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(IModList::ModStates)
+
+} // namespace MOBase
+
+#endif // IMODLIST_H
diff --git a/libs/uibase/include/uibase/imodrepositorybridge.h b/libs/uibase/include/uibase/imodrepositorybridge.h
new file mode 100644
index 0000000..f2bf539
--- /dev/null
+++ b/libs/uibase/include/uibase/imodrepositorybridge.h
@@ -0,0 +1,219 @@
+#ifndef INEXUSBRIDGE_H
+#define INEXUSBRIDGE_H
+
+#include "modrepositoryfileinfo.h"
+#include <QNetworkReply>
+#include <QObject>
+#include <QVariant>
+
+namespace MOBase
+{
+
+class QDLLEXPORT IModRepositoryBridge : public QObject
+{
+ Q_OBJECT
+public:
+ IModRepositoryBridge(QObject* parent = nullptr) : QObject(parent) {}
+
+ /**
+ * @brief request description for a mod
+ *
+ * @param modID id of the mod caller is interested in
+ * @param userData user data to be returned with the result
+ **/
+ virtual void requestDescription(QString gameName, int modID, QVariant userData) = 0;
+
+ /**
+ * @brief request a list of the files belonging to a mod
+ *
+ * @param modID id of the mod caller is interested in
+ * @param userData user data to be returned with the result
+ **/
+ virtual void requestFiles(QString gameName, int modID, QVariant userData) = 0;
+
+ /**
+ * @brief request info about a single file of a mod
+ *
+ * @param modID id of the mod caller is interested in
+ * @param fileID id of the file the caller is interested in
+ * @param userData user data to be returned with the result
+ **/
+ virtual void requestFileInfo(QString game, int modID, int fileID,
+ QVariant userData) = 0;
+
+ /**
+ * @brief request the download url of a file
+ *
+ * @param modID id of the mod caller is interested in
+ * @param fileID id of the file the caller is interested in
+ * @param userData user data to be returned with the result
+ **/
+ virtual void requestDownloadURL(QString gameName, int modID, int fileID,
+ QVariant userData) = 0;
+
+ /**
+ * @brief requestToggleEndorsement
+ * @param modID id of the mod caller is interested in
+ * @param userData user data to be returned with the result
+ */
+ virtual void requestToggleEndorsement(QString gameName, int modID, QString modVersion,
+ bool endorse, QVariant userData) = 0;
+
+private:
+ Q_DISABLE_COPY(IModRepositoryBridge)
+
+Q_SIGNALS:
+
+ /**
+ * @brief sent when the description for a mod is reported by the repository
+ * @param modID id of the mod for which the request was made
+ * @param userData the data that was included in the request
+ * @param resultData result data. this is a variant map. keys are strings. Value is
+ * usually also a string, wrapped in a variant.
+ * @note valid keys might change as the repository page is updated. For nexus, the
+ * following keys are valid as of this writing: 'allow_view', 'ip',
+ * 'one_week_ratings', 'date', 'pm_notify', 'OLD_mid', 'OLD_u_downloads', 'game_id',
+ * 'OLD_perm_use', 'mod_page_uri', 'allow_topics', 'has_hot_image', 'id',
+ * 'two_weeks_ratings', 'description', 'lastupdate', 'perm_convert', 'author',
+ * 'OLD_image', 'translation_of', 'OLD_mname', 'version', 'allow_rating',
+ * 'perm_useinstructions', 'featured_count', 'donate', 'type', 'perm_credits',
+ * 'hidden_reason', 'OLD_views', 'perm_upload', 'has_back_image', 'adult',
+ * 'allow_images', 'OLD_endorsements', 'OLD_size', 'name', 'commenting', 'moderate',
+ * 'language', 'perm_others', 'lastcomment', 'OLD_readme', 'summary', 'perm_modify',
+ * 'OLD_downloads', 'lock_comments', 'suggested_category', 'allow_tagging',
+ * 'published', 'perm_notes', 'category_id', 'thread_id', 'perm_use', 'wizard_steps'
+ * @note in the python interface use the onDescriptionAvailable call to register a
+ * callback for this signal. The signature of your callback must match the signature
+ * of this signal
+ * @note this interface is going to be changed at some point to replace resultData
+ * with a less "dynamic" data structure
+ */
+ void descriptionAvailable(QString gameName, int modID, QVariant userData,
+ QVariant resultData);
+
+ /**
+ * @brief sent when the list of files for a mod is reported by the repository
+ * @param modID id of the mod for which the request was made
+ * @param userData the data that was included in the request
+ * @param resultData a list of (already decoded) file information objects
+ * @note in the python interface use the onFilesAvailable call to register a callback
+ * for this signal. The signature of your callback must match the signature of this
+ * signal
+ */
+ void filesAvailable(QString gameName, int modID, QVariant userData,
+ const QList<ModRepositoryFileInfo*>& resultData);
+
+ /**
+ * @brief sent when information about a file is reported by the repository
+ * @param modID id of the mod for which the request was made
+ * @param fileID id of the the file for which information was requested
+ * @param userData the data that was included in the request
+ * @param resultData a variant map of information about the file.
+ * @note valid keys might change as the repository page is updated. For nexus, the
+ * following keys are valid as of this writing: 'count', 'requirements_alert',
+ * 'u_count', 'description', 'uri', 'size', 'owner_id', 'primary', 'manager',
+ * 'version', 'date', 'game_id', 'mod_id', 'category_id', 'id', 'name'
+ * @note in the python interface use the onFileInfoAvailable call to register a
+ * callback for this signal. The signature of your callback must match the signature
+ * of this signal
+ * @note if you intend to download this file you don't have to request this
+ * information manually. call IDownloadManager::startDownloadNexusFile and let the
+ * download manager figure things out
+ * @note this interface is going to be changed at some point to replace resultData
+ * with a less "dynamic" data structure
+ */
+ void fileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData,
+ QVariant resultData);
+
+ /**
+ * @brief sent when the list of download urls for a file is returned by the repository
+ * @param modID id of the mod for which the request was made
+ * @param fileID id of the file for which the downloads
+ * @param userData the data that was included in the request
+ * @param resultData a variant map of information about the url
+ * @note this function is not exposed to python. please use
+ * IDownloadManager::startDownloadNexusFile to download a file, this lets the download
+ * manager figure out the best server according to user preference and stuff
+ * @note this interface is going to be changed at some point to replace resultData
+ * with a less "dynamic" data structure
+ */
+ void downloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData,
+ QVariant resultData);
+
+ /**
+ * @brief sent when the endorsement data is returned from the API
+ * @param userData the data that was included in the request
+ * @param resultData new endorsement state as a boolean (wrapped in a qvariant)
+ * @note in the python interface use the onEndorsementsAvailable call to register a
+ * callback for this signal. The signature of your callback must match the signature
+ * of this signal
+ */
+ void endorsementsAvailable(QVariant userData, QVariant resultData);
+
+ /**
+ * @brief sent when the endorsement state of a mod was changed (only sent as a result
+ * of our request)
+ * @param modID id of the mod for which the request was made
+ * @param userData the data that was included in the request
+ * @param resultData new endorsement state as a boolean (wrapped in a qvariant)
+ * @note in the python interface use the onEndorsementToggled call to register a
+ * callback for this signal. The signature of your callback must match the signature
+ * of this signal
+ * @note this interface is going to be changed at some point to replace resultData
+ * with a boolean
+ */
+ void endorsementToggled(QString gameName, int modID, QVariant userData,
+ QVariant resultData);
+
+ /**
+ * @brief sent when the tracked mod data is returned from the API
+ * @param userData the data that was included in the request
+ * @param resultData new tracked state as a list of maps (keys: domain_name, mod_id)
+ * @note in the python interface use the ontrackedModsAvailable call to register a
+ * callback for this signal. The signature of your callback must match the signature
+ * of this signal
+ */
+ void trackedModsAvailable(QVariant userData, QVariant resultData);
+
+ /**
+ * @brief sent when the tracking state of a mod was changed (only sent as a result of
+ * our request)
+ * @param modID id of the mod for which the request was made
+ * @param userData the data that was included in the request
+ * @param tracked new tracking state
+ * @note in the python interface use the onTrackingToggled call to register a callback
+ * for this signal. The signature of your callback must match the signature of this
+ * signal
+ */
+ void trackingToggled(QString gameName, int modID, QVariant userData, bool tracked);
+
+ /**
+ * @brief sent when the tracking state of a mod was changed (only sent as a result of
+ * our request)
+ * @param modID id of the mod for which the request was made
+ * @param userData the data that was included in the request
+ * @param tracked new tracking state
+ * @note in the python interface use the onTrackingToggled call to register a callback
+ * for this signal. The signature of your callback must match the signature of this
+ * signal
+ */
+ void gameInfoAvailable(QString gameName, QVariant userData, QVariant resultData);
+
+ /**
+ * @brief sent when a request to nexus failed
+ * @param modID id of the mod for which the request was made
+ * @param fileID id of the file for which the request was made (ignore if the request
+ * was for the mod in general)
+ * @param userData the data that was included in the request
+ * @param errorMessage textual description of the problem
+ * @note in the python interface use the onDescriptionAvailable call to register a
+ * callback for this signal. The signature of your callback must match the signature
+ * of this signal
+ */
+ void requestFailed(QString gameName, int modID, int fileID, QVariant userData,
+ int errorCode, const QString& errorMessage);
+};
+
+} // namespace MOBase
+
+#endif // INEXUSBRIDGE_H
diff --git a/libs/uibase/include/uibase/imoinfo.h b/libs/uibase/include/uibase/imoinfo.h
new file mode 100644
index 0000000..a9ec3d8
--- /dev/null
+++ b/libs/uibase/include/uibase/imoinfo.h
@@ -0,0 +1,616 @@
+/*
+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 IMOINFO_H
+#define IMOINFO_H
+
+#include <QDir>
+#include <QList>
+#include <QMainWindow>
+#include <QString>
+#include <QVariant>
+#include <cstdint>
+#include <any>
+#include <functional>
+#include <memory>
+
+// Include utility.h for HANDLE/DWORD typedefs on Linux
+#include "utility.h"
+
+#include "game_features/game_feature.h"
+#include "guessedvalue.h"
+#include "imodlist.h"
+#include "iprofile.h"
+#include "versioninfo.h"
+#include "versioning.h"
+
+namespace MOBase
+{
+
+class IExecutablesList;
+class IFileTree;
+class IModInterface;
+class IModRepositoryBridge;
+class IDownloadManager;
+class IInstanceManager;
+class IPluginList;
+class IPlugin;
+class IPluginGame;
+class IGameFeatures;
+
+/**
+ * @brief Interface to class that provides information about the running session
+ * of Mod Organizer to be used by plugins
+ *
+ * When MO requires plugins but does not have a valid instance loaded (such as
+ * on first start in the instance creation dialog), init() will not be called at
+ * all, except for proxy plugins.
+ *
+ * In the case of proxy plugins, init() is called with a null IOrganizer.
+ */
+class QDLLEXPORT IOrganizer : public QObject
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief information about a virtualised file
+ */
+ struct FileInfo
+ {
+ QString filePath; /// full path to the file
+ QString archive; /// name of the archive if this file is in a BSA, otherwise this
+ /// is empty.
+ QStringList origins; /// list of origins containing this file. the first one is the
+ /// highest priority one
+ };
+
+public:
+ virtual ~IOrganizer() {}
+
+ // the directory for plugin data, typically plugins/data
+ //
+ static QString getPluginDataPath();
+
+ /**
+ * @return create a new nexus interface class
+ */
+ virtual IModRepositoryBridge* createNexusBridge() const = 0;
+
+ /**
+ * @return the name of the current instance (the directory name or "Portable" for
+ * portable instances)
+ */
+ virtual QString instanceName() const = 0;
+
+ /**
+ * @return name of the active profile or an empty string if no profile is loaded (yet)
+ */
+ virtual QString profileName() const = 0;
+
+ /**
+ * @return the (absolute) path to the active profile or an empty string if no profile
+ * is loaded (yet)
+ */
+ virtual QString profilePath() const = 0;
+
+ /**
+ * @return the (absolute) path to the download directory
+ */
+ virtual QString downloadsPath() const = 0;
+
+ /**
+ * @return the (absolute) path to the overwrite directory
+ */
+ virtual QString overwritePath() const = 0;
+
+ /**
+ * @return the (absolute) path to the base directory
+ */
+ virtual QString basePath() const = 0;
+
+ /**
+ * @return the (absolute) path to the mods directory
+ */
+ virtual QString modsPath() const = 0;
+
+ /**
+ * @return the running version of Mod Organizer
+ */
+ [[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
+ * @param name name of the new mod
+ * @return an interface that can be used to modify the mod. nullptr if the user
+ * canceled
+ * @note a popup asking the user to merge, rename or replace the mod is displayed if
+ * the mod already exists. That has to happen on the main thread and MO2 will deadlock
+ * if it happens on any other. If this needs to be called from another thread, use
+ * IModList::getMod() to verify the mod-name is unused first
+ */
+ virtual IModInterface* createMod(GuessedValue<QString>& name) = 0;
+
+ /**
+ * @brief get the game plugin matching the specified game
+ * @param gameName name of the game short name
+ * @return a game plugin, or nullptr if there is no match
+ */
+ virtual IPluginGame* getGame(const QString& gameName) const = 0;
+
+ /**
+ * @brief let the organizer know that a mod has changed
+ * @param the mod that has changed
+ */
+ virtual void modDataChanged(IModInterface* mod) = 0;
+
+ /**
+ * @brief Check if a plugin is enabled.
+ *
+ * @param pluginName Plugin to check.
+ *
+ * @return true if the plugin is enabled, false otherwise.
+ */
+ virtual bool isPluginEnabled(IPlugin* plugin) const = 0;
+
+ /**
+ * @brief Check if a plugin is enabled.
+ *
+ * @param pluginName Name of the plugin to check.
+ *
+ * @return true if the plugin is enabled, false otherwise.
+ */
+ virtual bool isPluginEnabled(QString const& pluginName) const = 0;
+
+ /**
+ * @brief Retrieve the specified setting 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 mod. You can not access settings for a plugin that isn't installed.
+ * @param key identifier of the setting
+ * @return the setting
+ * @note an invalid qvariant is returned if the setting has not been declared
+ */
+ virtual QVariant pluginSetting(const QString& pluginName,
+ const QString& key) const = 0;
+
+ /**
+ * @brief Set the specified setting for a plugin.
+ *
+ * This automatically emit pluginSettingChanged(), so you do not have to do it
+ * yourself.
+ *
+ * @param pluginName Name of the plugin for which to change a value. This should
+ * always be IPlugin::name() unless you have a really good reason to access data of
+ * another mod AND if you can verify that plugin is actually installed.
+ * @param key Identifier of the setting.
+ * @param value Value to set.
+ *
+ * @throw an exception is thrown if pluginName doesn't refer to an installed plugin.
+ */
+ virtual void setPluginSetting(const QString& pluginName, const QString& key,
+ const QVariant& value) = 0;
+
+ /**
+ * @brief retrieve the specified persistent value for a plugin
+ * @param pluginName name of the plugin for which to retrieve a value. This should
+ * always be IPlugin::name() unless you have a really good reason to access data of
+ * another mod.
+ * @param key identifier of the value
+ * @param def default value to return if the key is not (yet) set
+ * @return the value
+ * @note A persistent is an arbitrary value that the plugin can set and retrieve that
+ * is persistently stored by the main application. There is no UI for the user to
+ * change this value but (s)he can directly access the storage
+ */
+ virtual QVariant persistent(const QString& pluginName, const QString& key,
+ const QVariant& def = QVariant()) const = 0;
+
+ /**
+ * @brief set the specified persistent value for a plugin
+ * @param pluginName name of the plugin for which to change a value. This should
+ * always be IPlugin::name() unless you have a really good reason to access data of
+ * another mod AND if you can verify that plugin is actually installed
+ * @param key identifier of the value
+ * @param value value to set
+ * @param sync if true the storage is immediately written to disc. This costs
+ * performance but is safer against data loss
+ * @throw an exception is thrown if pluginName doesn't refer to an installed plugin
+ */
+ virtual void setPersistent(const QString& pluginName, const QString& key,
+ const QVariant& value, bool sync = true) = 0;
+
+ /**
+ * @return path to a directory where plugin data should be stored.
+ */
+ virtual QString pluginDataPath() const = 0;
+
+ /**
+ * @brief install a mod archive at the specified location
+ * @param fileName absolute file name of the mod to install
+ * @param nameSuggestion suggested name for this mod. This can still be changed by the
+ * user
+ * @return interface to the newly installed mod or nullptr if no installation took
+ * place (failure or use canceled
+ */
+ virtual IModInterface* installMod(const QString& fileName,
+ const QString& nameSuggestion = QString()) = 0;
+
+ /**
+ * @brief resolves a path relative to the virtual data directory to its absolute real
+ * path
+ * @param fileName path to resolve
+ * @return the absolute real path or an empty string
+ */
+ virtual QString resolvePath(const QString& fileName) const = 0;
+
+ /**
+ * @brief retrieves a list of (virtual) subdirectories for a path (relative to the
+ * data directory)
+ * @param directoryName relative path to the directory to list
+ * @return a list of directory names
+ */
+ virtual QStringList listDirectories(const QString& directoryName) const = 0;
+
+ /**
+ * @brief find files in the virtual directory matching the filename filter
+ * @param path the path to search in
+ * @param filter filter function to match against
+ * @return a list of matching files
+ */
+ virtual QStringList
+ findFiles(const QString& path,
+ const std::function<bool(const QString&)>& filter) const = 0;
+
+ /**
+ * @brief find files in the virtual directory matching the filename filter
+ * @param path the path to search in
+ * @param filters list of glob filters to match against
+ * @return a list of matching files
+ */
+ virtual QStringList findFiles(const QString& path,
+ const QStringList& filters) const = 0;
+
+ /**
+ * @brief retrieve the file origins for the speicified file. The origins are listed
+ * with their internal name
+ * @return list of origins that contain the specified file, sorted by their priority
+ * @note the internal name of a mod can differ from the display name for
+ * disambiguation
+ */
+ virtual QStringList getFileOrigins(const QString& fileName) const = 0;
+
+ /**
+ * @brief find files in the virtual directory matching the specified complex filter
+ * @param path the path to search in
+ * @param filter filter function to match against
+ * @return a list of matching files
+ * @note this function is more expensive than the one filtering by name so use the
+ * other one if it suffices
+ */
+ virtual QList<FileInfo>
+ findFileInfos(const QString& path,
+ const std::function<bool(const FileInfo&)>& filter) const = 0;
+
+ /**
+ * @return a IFileTree representing the virtual file tree.
+ */
+ virtual std::shared_ptr<const MOBase::IFileTree> virtualFileTree() const = 0;
+
+ /**
+ * @return interface to the instance manager
+ */
+ virtual IInstanceManager* instanceManager() const = 0;
+
+ /**
+ * @return interface to the download manager
+ */
+ virtual IDownloadManager* downloadManager() const = 0;
+
+ /**
+ * @return interface to the list of plugins (esps, esms, and esls)
+ */
+ virtual IPluginList* pluginList() const = 0;
+
+ /**
+ * @return interface to the list of mods
+ */
+ virtual IModList* modList() const = 0;
+
+ /**
+ * @return interface to the list of executables
+ */
+ virtual IExecutablesList* executablesList() const = 0;
+
+ /**
+ * @return interface to the active profile
+ */
+ virtual std::shared_ptr<IProfile> profile() const = 0;
+
+ /**
+ * @return list of names of all profiles
+ */
+ virtual QStringList profileNames() const = 0;
+
+ /**
+ * @brief retrieve a profile by name
+ *
+ * @param name name of the profile
+ *
+ * @return the profile with the specified name or nullptr if not found
+ */
+ virtual std::shared_ptr<const IProfile> getProfile(const QString& name) const = 0;
+
+ /**
+ * @return interface to game features.
+ */
+ virtual IGameFeatures* gameFeatures() const = 0;
+
+ /**
+ * @brief runs a program using the virtual filesystem
+ *
+ * @param executable either the name of an executable configured in MO, or
+ * a path to an executable; if relative, it is resolved
+ * against the game directory
+ *
+ * @param args arguments to pass to the executable; if this is empty
+ * and `executable` refers to a configured executable,
+ * its arguments are used
+ *
+ * @param cwd working directory for the executable; if this is empty,
+ * it is set to either the cwd set by the user in the
+ * configured executable (if any) or the directory of the
+ * executable
+ *
+ * @param profile name of the profile to use; defaults to the active
+ * profile
+ *
+ * @param forcedCustomOverwrite the name of the mod to set as the custom
+ * overwrite directory, regardless of what the
+ * profile has configured
+ *
+ * @param ignoreCustomOverwrite if `executable` is the name of a configured
+ * executable, ignores the executable's custom
+ * overwrite
+ *
+ * @return a handle to the process that was started or INVALID_HANDLE_VALUE
+ * if the application failed to start.
+ */
+ virtual HANDLE startApplication(const QString& executable,
+ const QStringList& args = QStringList(),
+ const QString& cwd = "", const QString& profile = "",
+ const QString& forcedCustomOverwrite = "",
+ bool ignoreCustomOverwrite = false) = 0;
+
+ /**
+ * @brief blocks until the given process has completed
+ *
+ * @param handle the process to wait for
+ * @param refresh whether MO should refresh after the process completed
+ * @param exitCode the exit code of the process after it ended
+ *
+ * @return true if the process completed successfully
+ *
+ * @note this will always show the lock overlay, regardless of whether the
+ * user has disabled locking in the setting, so use this with care;
+ * note that the lock overlay will always allow the user to unlock, in
+ * which case this will return false
+ */
+ virtual bool waitForApplication(HANDLE handle, bool refresh = true,
+ LPDWORD exitCode = nullptr) const = 0;
+
+ /**
+ * @brief Refresh the internal mods file structure from disk. This includes the mod
+ * list, the plugin list, data tab and other smaller things like problems button (same
+ * as pressing F5).
+ *
+ * @note The main part of the refresh of the mods file strcuture, modlist and
+ * pluginlist is done asynchronously, so you should not expect them to be up-to-date
+ * when this function returns.
+ *
+ * @param saveChanges If true, the relevant profile information is saved first
+ * (enabled mods and their order).
+ */
+ virtual void refresh(bool saveChanges = true) = 0;
+
+ /**
+ * @brief get the currently managed game info
+ */
+ virtual MOBase::IPluginGame const* managedGame() const = 0;
+
+ /**
+ * @brief Add a new callback to be called when an application is about to be run.
+ *
+ * Parameters of the callback:
+ * - Path (absolute) to the application to be run.
+ * - [Optional] Working directory for the run.
+ * - [Optional] Argument for the binary.
+ *
+ * The callback can return false to prevent the application from being launched.
+ *
+ * @param func Function to be called when an application is run.
+ */
+ virtual bool onAboutToRun(const std::function<bool(const QString&)>& func) = 0;
+ virtual bool onAboutToRun(
+ const std::function<bool(const QString&, const QDir&, const QString&)>& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when an has finished running.
+ *
+ * Parameters of the callback:
+ * - Path (absolute) to the application that has finished running.
+ * - Exit code of the application.
+ *
+ *
+ * @param func Function to be called when an application is run.
+ */
+ virtual bool
+ onFinishedRun(const std::function<void(const QString&, unsigned int)>& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when the user interface has been
+ * initialized.
+ *
+ * Parameters of the callback:
+ * - The main window of the application.
+ *
+ * @param func Function to be called when the user interface has been initialized.
+ */
+ virtual bool
+ onUserInterfaceInitialized(std::function<void(QMainWindow*)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when the next refresh finishes (or
+ * immediately if possible).
+ *
+ * @param func Callback.
+ * @param immediateIfPossible If true and no refresh is in progress, the callback will
+ * be called immediately.
+ */
+ virtual bool onNextRefresh(std::function<void()> const& func,
+ bool immediateIfPossible = true) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a new profile is created.
+ *
+ * Parameters of the callback:
+ * - The created profile (can be a temporary object, so it should not be stored).
+ *
+ * @param func Function to be called when a profile is created.
+ *
+ */
+ virtual bool onProfileCreated(std::function<void(IProfile*)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a profile is renamed.
+ *
+ * Parameters of the callback:
+ * - The renamed profile.
+ * - The old name of the profile.
+ * - The new name of the profile.
+ *
+ * @param func Function to be called when a profile is renamed.
+ *
+ */
+ virtual bool onProfileRenamed(
+ std::function<void(IProfile*, QString const&, QString const&)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a profile is removed.
+ *
+ * Parameters of the callback:
+ * - The name of the removed profile.
+ *
+ * The function is called after the profile has been removed, so the profile is not
+ * accessible anymore.
+ *
+ * @param func Function to be called when a profile is removed.
+ *
+ */
+ virtual bool onProfileRemoved(std::function<void(QString const&)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when the current profile is changed.
+ *
+ * Parameters of the callback:
+ * - The old profile. Can be a null pointer if no profile was set (e.g. at startup).
+ * - The new profile, cannot be null.
+ *
+ * The function is called when the profile is changed but some operations related to
+ * the profile might not be finished when this is called (e.g., the virtual file
+ * system might not be up-to-date).
+ *
+ * @param func Function to be called when the current profile change.
+ *
+ */
+ virtual bool
+ onProfileChanged(std::function<void(IProfile*, IProfile*)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a plugin setting is changed.
+ *
+ * Parameters of the callback:
+ * - Name of the plugin.
+ * - Name of the setting.
+ * - Old value of the setting. Can be a default-constructed (invalid) QVariant if
+ * the setting did not exist before.
+ * - New value of the setting. Can be a default-constructed (invalid) QVariant if
+ * the setting has been removed.
+ *
+ * @param func Function to be called when a plugin setting is changed.
+ */
+ virtual bool onPluginSettingChanged(
+ std::function<void(QString const&, const QString& key, const QVariant&,
+ const QVariant&)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a plugin is enabled.
+ *
+ * Parameters of the callback:
+ * - The enabled plugin.
+ *
+ * @param func Function to be called when a plugin is enabled.
+ */
+ virtual bool onPluginEnabled(std::function<void(const IPlugin*)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when the specified plugin is enabled.
+ *
+ * @param name Name of the plugin to watch.
+ * @param func Function to be called when a plugin is enabled.
+ */
+ virtual bool onPluginEnabled(const QString& pluginName,
+ std::function<void()> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when a plugin is disabled.
+ *
+ * Parameters of the callback:
+ * - The disabled plugin.
+ *
+ * @param func Function to be called when a plugin is disabled.
+ */
+ virtual bool onPluginDisabled(std::function<void(const IPlugin*)> const& func) = 0;
+
+ /**
+ * @brief Add a new callback to be called when the specified plugin is disabled.
+ *
+ * @param name Name of the plugin to watch.
+ * @param func Function to be called when a plugin is disabled.
+ */
+ virtual bool onPluginDisabled(const QString& pluginName,
+ std::function<void()> const& func) = 0;
+};
+
+} // namespace MOBase
+
+namespace MOBase::details
+{
+// called from MO
+QDLLEXPORT void setPluginDataPath(const QString& s);
+} // namespace MOBase::details
+
+#endif // IMOINFO_H
diff --git a/libs/uibase/include/uibase/iplugin.h b/libs/uibase/include/uibase/iplugin.h
new file mode 100644
index 0000000..aae89dc
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugin.h
@@ -0,0 +1,146 @@
+/*
+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 IPLUGIN_H
+#define IPLUGIN_H
+
+#include "imoinfo.h"
+#include "pluginrequirements.h"
+#include "pluginsetting.h"
+#include "versioninfo.h"
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <vector>
+
+namespace MOBase
+{
+
+class IPlugin
+{
+public:
+ // For easier access in child class:
+ using Requirements = PluginRequirementFactory;
+
+public:
+ virtual ~IPlugin() {}
+
+ /**
+ * @brief Initialize the plugin.
+ *
+ * Note that this function may never be called if no IOrganizer is available
+ * at that time, such as when creating the first instance in MO. For proxy
+ * plugins, init() is always called, but given a null IOrganizer.
+ *
+ * Plugins will probably want to store the organizer pointer. It is guaranteed
+ * to be valid as long as the plugin is loaded.
+ *
+ * These functions may be called before init():
+ * - name()
+ * - see IPluginGame for more
+ *
+ * @return false if the plugin could not be initialized, true otherwise.
+ */
+ virtual bool init(IOrganizer* organizer) = 0;
+
+ /**
+ * this function may be called before init()
+ *
+ * @return the internal name of this plugin (used for example in the settings menu).
+ *
+ * @note Please ensure you use a name that will not change. Do NOT include a version
+ * number in the name. Do NOT use a localizable string (tr()) here. Settings for
+ * example are tied to this name, if you rename your plugin you lose settings users
+ * made.
+ */
+ virtual QString name() const = 0;
+
+ /**
+ * @return the localized name for this plugin.
+ *
+ * @note Unlike name(), this method can (and should!) return a localized name for the
+ * plugin.
+ * @note This method returns name() by default.
+ */
+ 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<std::shared_ptr<const IPluginRequirement>> 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.
+ *
+ * @note Plugin can store "hidden" (from the user) settings using
+ * IOrganizer::persistent / IOrganizer::setPersistent.
+ */
+ virtual QList<PluginSetting> settings() const = 0;
+
+ /**
+ * @return whether the plugin should be enabled by default
+ */
+ virtual bool enabledByDefault() const { return true; }
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPlugin, "com.tannin.ModOrganizer.Plugin/2.0")
+
+#endif // IPLUGIN_H
diff --git a/libs/uibase/include/uibase/iplugindiagnose.h b/libs/uibase/include/uibase/iplugindiagnose.h
new file mode 100644
index 0000000..5a9340c
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugindiagnose.h
@@ -0,0 +1,103 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2013 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 IPLUGINDIAGNOSE_H
+#define IPLUGINDIAGNOSE_H
+
+#include <QList>
+#include <QObject>
+#include <QString>
+#include <functional>
+#include <vector>
+
+namespace MOBase
+{
+
+/**
+ * @brief A plugin that creates problem reports to be displayed in the UI.
+ * This can be used to report problems related to the same plugin (which implements
+ * further interfaces) or as a stand-alone diagnosis tool. This does not derive from
+ * IPlugin to prevent multiple inheritance issues. For stand-alone diagnosis plugins,
+ * derive from IPlugin and IPluginDiagnose
+ */
+class IPluginDiagnose
+{
+public:
+ /**
+ * @return a list of keys of active problems
+ * @note this is not expected to be called if isActive returns false
+ */
+ virtual std::vector<unsigned int> activeProblems() const = 0;
+
+ /**
+ * @brief retrieve a short description for the specified problem for the overview
+ * page. HTML syntax is supported.
+ * @param key the identifier of the problem as reported by activeProblems()
+ * @return a (short) description text
+ * @throw should throw an exception if the key is not valid
+ */
+ virtual QString shortDescription(unsigned int key) const = 0;
+
+ /**
+ * @brief retrieve the full description for the specified problem. HTML syntax is
+ * supported.
+ * @param key the identifier of the problem as reported by activeProblems()
+ * @return a (long) description text
+ * @throw should throw an exception if the key is not valid
+ */
+ virtual QString fullDescription(unsigned int key) const = 0;
+
+ /**
+ * @param key the identifier of the problem as reported by activeProblems()
+ * @return true if this plugin provides a guide to fix the issue
+ * @throw should throw an exception if the key is not valid
+ */
+ virtual bool hasGuidedFix(unsigned int key) const = 0;
+
+ /**
+ * @brief start the guided fix for the specified problem
+ * @param key the identifier of the problem as reported by activeProblems()
+ * @throw should throw an exception if the key is not valid or if there is no guided
+ * fix for the issue
+ */
+ virtual void startGuidedFix(unsigned int key) const = 0;
+
+ /**
+ * @brief Register the callback to be called when this plugin is invalidated.
+ *
+ * Only one callback can be activate at a time.
+ */
+ void onInvalidated(std::function<void()> callback) { m_OnInvalidated = callback; }
+
+ virtual ~IPluginDiagnose() {}
+
+protected:
+ void invalidate() { m_OnInvalidated(); }
+
+private:
+ std::function<void()> m_OnInvalidated;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginDiagnose,
+ "com.tannin.ModOrganizer.PluginDiagnose/1.1")
+
+#endif // IPLUGINDIAGNOSE_H
diff --git a/libs/uibase/include/uibase/ipluginfilemapper.h b/libs/uibase/include/uibase/ipluginfilemapper.h
new file mode 100644
index 0000000..350aa71
--- /dev/null
+++ b/libs/uibase/include/uibase/ipluginfilemapper.h
@@ -0,0 +1,49 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2015 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 IPLUGINFILEMAPPER_H
+#define IPLUGINFILEMAPPER_H
+
+#include "filemapping.h"
+#include "iplugin.h"
+
+namespace MOBase
+{
+
+/**
+ * brief A plugin that adds virtual file links
+ * This does not derive from IPlugin to prevent multiple inheritance issues.
+ * For stand-alone mapping plugins, derive from IPlugin and IPluginDiagnose
+ */
+class IPluginFileMapper
+{
+public:
+ /**
+ * @return a list of file maps
+ */
+ virtual MappingType mappings() const = 0;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginFileMapper,
+ "com.tannin.ModOrganizer.PluginFileMapper/2.0")
+
+#endif // IPLUGINDIAGNOSE_H
diff --git a/libs/uibase/include/uibase/iplugingame.h b/libs/uibase/include/uibase/iplugingame.h
new file mode 100644
index 0000000..0fa2508
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugingame.h
@@ -0,0 +1,380 @@
+#ifndef IPLUGINGAME_H
+#define IPLUGINGAME_H
+
+#include "executableinfo.h"
+#include "iplugin.h"
+#include "isavegame.h"
+
+class QIcon;
+class QUrl;
+class QString;
+
+#include <any>
+#include <cstdint>
+#include <memory>
+#include <typeindex>
+#include <unordered_map>
+#include <vector>
+
+namespace MOBase
+{
+
+// Game plugins can be loaded without an IOrganizer being available, in which
+// case detectGame() is called, but not init().
+//
+// These functions may be called before init() (after detectGame()):
+// - gameName()
+// - isInstalled()
+// - gameIcon()
+// - gameDirectory()
+// - dataDirectory()
+// - gameVariants()
+// - looksValid()
+// - see IPlugin::init() for more
+//
+//
+class IPluginGame : public QObject, public IPlugin
+{
+ Q_INTERFACES(IPlugin)
+
+public:
+ enum class LoadOrderMechanism
+ {
+ None,
+ FileTime,
+ PluginsTxt
+ };
+
+ enum class SortMechanism
+ {
+ NONE,
+ MLOX,
+ BOSS,
+ LOOT
+ };
+
+ enum ProfileSetting
+ {
+ MODS = 0x01,
+ CONFIGURATION = 0x02,
+ SAVEGAMES = 0x04,
+ PREFER_DEFAULTS = 0x08
+ };
+
+ Q_DECLARE_FLAGS(ProfileSettings, ProfileSetting)
+
+public:
+ // Game plugin should not have requirements:
+ std::vector<std::shared_ptr<const IPluginRequirement>>
+ requirements() const final override
+ {
+ return {};
+ }
+
+ // Game plugin can not be disabled
+ bool enabledByDefault() const final override { return true; }
+
+ /**
+ * this function may be called before init()
+ *
+ * @return name of the game
+ */
+ virtual QString gameName() const = 0;
+
+ /**
+ * used to override display-specific text in place of the gameName
+ * for example the text for the main window or the initial instance creation game
+ * plugin selection/list
+ *
+ * added for future translation purposes or to make plugins visually more accurate in
+ * the ui
+ *
+ * @return display-specific name of the game
+ */
+ virtual QString displayGameName() const { return gameName(); }
+
+ /**
+ * @brief Detect the game.
+ *
+ * This method is the first method called for game plugins (before init()). The
+ * following methods can be called after detectGame() but before init():
+ * - gameName()
+ * - isInstalled()
+ * - gameIcon()
+ * - gameDirectory()
+ * - dataDirectory()
+ * - gameVariants()
+ * - looksValid()
+ * - see IPlugin::init() for more
+ */
+ virtual void detectGame() = 0;
+
+ /**
+ * @brief initialize a profile for this game
+ * @param directory the directory where the profile is to be initialized
+ * @param settings parameters for how the profile should be initialized
+ * @note the MO app does not yet support virtualizing only specific aspects but
+ * plugins should be written with this future functionality in mind
+ * @note this function will be used to initially create a profile, potentially to
+ * repair it or upgrade/downgrade it so the implementations have to gracefully handle
+ * the case that the directory already contains files!
+ */
+ virtual void initializeProfile(const QDir& directory,
+ ProfileSettings settings) const = 0;
+
+ /**
+ * @brief List save games in the specified folder.
+ *
+ * @param folder The folder containing the saves.
+ *
+ * @return the list of saves in the specified folder.
+ */
+ /**
+ * @return file extension of save games for this game
+ */
+ virtual std::vector<std::shared_ptr<const ISaveGame>>
+ listSaves(QDir folder) const = 0;
+
+ /**
+ * this function may be called before init()
+ *
+ * @return true if this game has been discovered as installed, false otherwise
+ */
+ virtual bool isInstalled() const = 0;
+
+ /**
+ * this function may be called before init()
+ *
+ * @return an icon for this game
+ */
+ virtual QIcon gameIcon() const = 0;
+
+ /**
+ * this function may be called before init()
+ *
+ * @return directory to the game installation
+ */
+ virtual QDir gameDirectory() const = 0;
+
+ /**
+ * this function may be called before init()
+ *
+ * @return directory where the game expects to find its data files
+ */
+ virtual QDir dataDirectory() const = 0;
+
+ virtual QString modDataDirectory() const { return ""; }
+
+ /**
+ * this function may be called before init()
+ *
+ * @return directories where we may find data files outside the main location
+ */
+ virtual QMap<QString, QDir> secondaryDataDirectories() const { return {}; }
+
+ /**
+ * @brief set the path to the managed game
+ * @param path to the game
+ * @note this will be called by by MO to set the concrete path of the game. This is
+ * particularly relevant if the path wasn't auto-detected but had to be set manually
+ * by the user
+ */
+ virtual void setGamePath(const QString& path) = 0;
+
+ /**
+ * @return directory of the documents folder where configuration files and such for
+ * this game reside
+ */
+ virtual QDir documentsDirectory() const = 0;
+
+ /**
+ * @return path to where save games are stored.
+ */
+ virtual QDir savesDirectory() const = 0;
+
+ /**
+ * @return list of automatically discovered executables of the game itself and tools
+ * surrounding it
+ */
+ virtual QList<ExecutableInfo> executables() const { return {}; }
+
+ /**
+ * @brief Get the default list of libraries that can be force loaded with executables
+ */
+ virtual QList<ExecutableForcedLoadSetting> executableForcedLoads() const = 0;
+
+ /**
+ * @return steam app id for this game. Should be empty for games not available on
+ * steam
+ * @note if a game is available in multiple versions those might have different app
+ * ids. the plugin should try to return the right one
+ */
+ virtual QString steamAPPId() const { return ""; }
+
+ /**
+ * @return list of plugins that are part of the game and not considered optional
+ */
+ virtual QStringList primaryPlugins() const { return {}; }
+
+ /**
+ * @return list of plugins enabled by the game but not in a strict load order
+ */
+ virtual QStringList enabledPlugins() const { return {}; }
+
+ /**
+ * this function may be called before init()
+ *
+ * @return list of game variants
+ * @note if there are multiple variants of a game (and the variants make a difference
+ * to the plugin) like a regular one and a GOTY-edition the plugin can return a list
+ * of them and the user gets to chose which one he owns.
+ */
+ virtual QStringList gameVariants() const { return {}; }
+
+ /**
+ * @brief if there are multiple game variants (returned by gameVariants) this will get
+ * called on start with the user-selected game edition
+ * @param variant the game edition selected by the user
+ */
+ virtual void setGameVariant(const QString& variant) = 0;
+
+ /**
+ * @brief Get the name of the executable that gets run
+ */
+ virtual QString binaryName() const = 0;
+
+ /**
+ * @brief Get the 'short' name of the game
+ *
+ * the short name of the game is used for - save ames, registry entries and
+ * nexus mod pages as far as I can see.
+ */
+ virtual QString gameShortName() const = 0;
+
+ /**
+ * @brief game name that's passed to the LOOT cli --game argument
+ *
+ * only applicable for games using LOOT based sorting
+ * defaults to gameShortName()
+ */
+ virtual QString lootGameName() const { return gameShortName(); }
+
+ /**
+ * @brief Get any primary alternative 'short' name for the game
+ *
+ * this is used to determine if a Nexus (or other) download source should be
+ * considered a 'primary' source for the game so that it isn't flagged as an
+ * alternative source
+ */
+ virtual QStringList primarySources() const { return {}; }
+
+ /**
+ * @brief Get any valid 'short' name for the game
+ *
+ * this is used to determine if a Nexus download is valid for the current game
+ * not all game variants have their own nexus pages and others can handle downloads
+ * from other nexus game pages and should be allowed
+ *
+ * the short name should be considered the primary handler for a directly supported
+ * game for puroses of auto-launching an instance
+ */
+ virtual QStringList validShortNames() const { return {}; }
+
+ /**
+ * @brief Get the 'short' name of the game
+ *
+ * the short name of the game is used for - save ames, registry entries and
+ * nexus mod pages as far as I can see.
+ */
+ virtual QString gameNexusName() const { return ""; }
+
+ /**
+ * @brief Get the list of .ini files this game uses
+ *
+ * @note It is important that the 'main' .ini file comes first in this list
+ */
+ virtual QStringList iniFiles() const { return {}; }
+
+ /**
+ * @brief Get a list of esp/esm files that are part of known dlcs
+ */
+ virtual QStringList DLCPlugins() const { return {}; }
+
+ /**
+ * @brief Get the current list of active Creation Club plugins
+ */
+ virtual QStringList CCPlugins() const { return {}; }
+
+ /*
+ * @brief determine the load order mechanism used by this game.
+ *
+ * @note this may throw an exception if the mechanism can't be determined
+ */
+ virtual LoadOrderMechanism loadOrderMechanism() const
+ {
+ return LoadOrderMechanism::None;
+ }
+
+ /**
+ * @brief determine the sorting mech
+ */
+ virtual SortMechanism sortMechanism() const { return SortMechanism::NONE; }
+
+ /**
+ * @brief Get the Nexus ID of Mod Organizer
+ */
+ virtual int nexusModOrganizerID() const { return 0; }
+
+ /**
+ * @brief Get the Nexus Game ID
+ */
+ virtual int nexusGameID() const = 0;
+
+ /**
+ * this function may be called before init()
+ *
+ * @brief See if the supplied directory looks like a valid game
+ */
+ virtual bool looksValid(QDir const&) const = 0;
+
+ /**
+ * @brief Get version of program
+ */
+ virtual QString gameVersion() const = 0;
+
+ /**
+ * @brief Get the name of the game launcher
+ */
+ virtual QString getLauncherName() const = 0;
+
+ /**
+ * @brief Get a URL for the support page for the game
+ */
+ virtual QString getSupportURL() const { return ""; }
+
+ /**
+ * @brief Gets a virtualization mapping for mod directories
+ *
+ * @note Maps internal mod directories to a list of paths
+ * @default Root directory maps to game data path(s)
+ */
+ virtual QMap<QString, QStringList> getModMappings() const
+ {
+ QMap<QString, QStringList> map;
+ QStringList dataDirs = {dataDirectory().absolutePath()};
+ for (auto path : secondaryDataDirectories()) {
+ dataDirs.append(path.absolutePath());
+ }
+ map[""] = dataDirs;
+ return map;
+ }
+};
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(IPluginGame::ProfileSettings)
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginGame, "com.tannin.ModOrganizer.PluginGame/2.0")
+Q_DECLARE_METATYPE(MOBase::IPluginGame const*)
+
+#endif // IPLUGINGAME_H
diff --git a/libs/uibase/include/uibase/iplugingamefeatures.h b/libs/uibase/include/uibase/iplugingamefeatures.h
new file mode 100644
index 0000000..553d302
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugingamefeatures.h
@@ -0,0 +1,61 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2024 MO2 Team. 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 IPLUGINGAMEFEATURES_H
+#define IPLUGINGAMEFEATURES_H
+
+namespace MOBase
+{
+
+class BSAInvalidation;
+class DataArchives;
+class GamePlugins;
+class LocalSavegames;
+class ModDataChecker;
+class ModDataContent;
+class SaveGameInfo;
+class ScriptExtender;
+class UnmanagedMods;
+
+/**
+ * @brief A plugin to implement game features.
+ *
+ */
+class IPluginGameFeautres
+{
+public:
+ /**
+ * Retrieve the priority of these game features.
+ *
+ * For mergeable features, e.g., ModDataChecker, this indicates the order in which the
+ * features are merged, otherwise, it indicates which plugin should provide the
+ * feature.
+ *
+ * The managed game always has lowest priority.
+ *
+ * @return the priority of this set of features.
+ */
+ virtual int priority() = 0;
+};
+
+} // namespace MOBase
+
+#endif // IPLUGINDIAGNOSE_H
+#pragma once
diff --git a/libs/uibase/include/uibase/iplugininstaller.h b/libs/uibase/include/uibase/iplugininstaller.h
new file mode 100644
index 0000000..d033fe9
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugininstaller.h
@@ -0,0 +1,157 @@
+/*
+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 IPLUGININSTALLER_H
+#define IPLUGININSTALLER_H
+
+#include <set>
+
+#include "ifiletree.h"
+#include "imodinterface.h"
+#include "iplugin.h"
+
+namespace MOBase
+{
+
+class IInstallationManager;
+
+class IPluginInstaller : public IPlugin
+{
+
+ Q_INTERFACES(IPlugin)
+
+public:
+ enum EInstallResult
+ {
+ RESULT_SUCCESS,
+ RESULT_FAILED,
+ RESULT_CANCELED,
+ RESULT_MANUALREQUESTED,
+ RESULT_NOTATTEMPTED,
+ RESULT_SUCCESSCANCEL,
+ RESULT_CATEGORYREQUESTED
+ };
+
+public:
+ IPluginInstaller() : m_ParentWidget(nullptr), m_InstallationManager(nullptr) {}
+
+ /**
+ * @brief Retrieve the priority of this installer. If multiple installers are able
+ * to handle an archive, the one with the highest priority wins.
+ *
+ * @return the priority of this installer.
+ */
+ virtual unsigned int priority() const = 0;
+
+ /**
+ * @return true if this plugin should be treated as a manual installer if the user
+ * explicitly requested one. A manual installer should offer the user maximum amount
+ * of customizability.
+ */
+ virtual bool isManualInstaller() const = 0;
+
+ /**
+ * @brief Method calls at the start of the installation process, before any other
+ * methods. This method is only called once per installation process, even for
+ * recursive installations (e.g. with the bundle installer).
+ *
+ * @param archive Path to the archive that is going to be installed.
+ * @param reinstallation True if this is a reinstallation, false otherwise.
+ * @param mod A currently installed mod corresponding to the archive being installed,
+ * or a null if there is no such mod.
+ *
+ * @note If `reinstallation` is true, then the given mod is the mod being reinstalled
+ * (the one selected by the user). If `reinstallation` is false and `currentMod` is
+ * not null, then it corresponds to a mod MO2 thinks corresponds to the archive (e.g.
+ * based on matching Nexus ID or name).
+ * @note The default implementation does nothing.
+ */
+ virtual void onInstallationStart([[maybe_unused]] QString const& archive,
+ [[maybe_unused]] bool reinstallation,
+ [[maybe_unused]] IModInterface* currentMod)
+ {}
+
+ /**
+ * @brief Method calls at the end of the installation process. This method is only
+ * called once per installation process, even for recursive installations (e.g. with
+ * the bundle installer).
+ *
+ * @param result The result of the installation.
+ * @param mod If the installation succeeded (result is RESULT_SUCCESS), contains the
+ * newly installed mod, otherwise it contains a null pointer.
+ *
+ * @note The default implementation does nothing.
+ */
+ virtual void onInstallationEnd([[maybe_unused]] EInstallResult result,
+ [[maybe_unused]] IModInterface* newMod)
+ {}
+
+ /**
+ * @brief Test if the archive represented by the tree parameter can be installed
+ * through this installer.
+ *
+ * @param tree a directory tree representing the archive.
+ *
+ * @return true if this installer can handle the archive.
+ */
+ virtual bool isArchiveSupported(std::shared_ptr<const IFileTree> tree) const = 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.
+ */
+ virtual void setParentWidget(QWidget* widget) { m_ParentWidget = widget; }
+
+ /**
+ * @brief Sets the installation manager responsible for the installation process
+ * it can be used by plugins to access utility functions.
+ *
+ * @param manager The new installation manager.
+ */
+ void setInstallationManager(IInstallationManager* manager)
+ {
+ m_InstallationManager = manager;
+ }
+
+protected:
+ /**
+ * @return the parent widget that the tool should use to create new dialogs and
+ * widgets.
+ */
+ QWidget* parentWidget() const { return m_ParentWidget; }
+
+ /**
+ * @return the manager responsible for the installation process.
+ */
+ IInstallationManager* manager() const { return m_InstallationManager; }
+
+private:
+ QWidget* m_ParentWidget;
+ IInstallationManager* m_InstallationManager;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginInstaller,
+ "com.tannin.ModOrganizer.PluginInstaller/1.0")
+
+#endif // IPLUGININSTALLER_H
diff --git a/libs/uibase/include/uibase/iplugininstallercustom.h b/libs/uibase/include/uibase/iplugininstallercustom.h
new file mode 100644
index 0000000..204639f
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugininstallercustom.h
@@ -0,0 +1,79 @@
+/*
+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 IPLUGININSTALLERCUSTOM_H
+#define IPLUGININSTALLERCUSTOM_H
+
+#include "guessedvalue.h"
+#include "iplugininstaller.h"
+
+namespace MOBase
+{
+
+/**
+ * Custom installer for mods. Custom installers receive the archive name and have to go
+ * from there. They have to be able to extract the archive themself.
+ * Plugins implementing this interface have to contain the following line in the class
+ * declaration: Q_Interfaces(IPlugin IPluginInstaller IPluginInstallerCustom)
+ */
+class IPluginInstallerCustom : public QObject, public IPluginInstaller
+{
+
+ Q_INTERFACES(IPluginInstaller)
+
+public:
+ /**
+ * @brief test if the archive represented by the file name can be installed through
+ * this installer
+ * @param filename of the archive
+ * @return true if this installer can handle the archive
+ * @note this is only called if the archive couldn't be opened by the caller,
+ * otherwise IPluginInstaller::isArchiveSupported(const DirectoryTree &tree) is called
+ */
+ virtual bool isArchiveSupported(const QString& archiveName) const = 0;
+
+ /**
+ * @return returns a list of file extensions that may be supported by this installer
+ */
+ virtual std::set<QString> supportedExtensions() const = 0;
+
+ /**
+ * install call
+ * @param modName name of the mod to install. As an input parameter this is the
+ * suggested name (i.e. from meta data) The installer may change this parameter to
+ * rename the mod)
+ * @param filename of the archive
+ * @param version version of the mod. May be empty if the version is not yet known.
+ * The plugin is responsible for setting the version on the created mod
+ * @param nexusID id of the mod or -1 if unknown. The plugin is responsible for
+ * setting the mod id for the created mod
+ * @return result of the installation process
+ */
+ virtual EInstallResult install(GuessedValue<QString>& modName, QString gameName,
+ const QString& archiveName, const QString& version,
+ int nexusID) = 0;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginInstallerCustom,
+ "com.tannin.ModOrganizer.PluginInstallerCustom/1.0")
+
+#endif // IPLUGININSTALLERCUSTOM_H
diff --git a/libs/uibase/include/uibase/iplugininstallersimple.h b/libs/uibase/include/uibase/iplugininstallersimple.h
new file mode 100644
index 0000000..a8795fc
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugininstallersimple.h
@@ -0,0 +1,69 @@
+/*
+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 IPLUGININSTALLERSIMPLE_H
+#define IPLUGININSTALLERSIMPLE_H
+
+#include "guessedvalue.h"
+#include "iplugininstaller.h"
+
+namespace MOBase
+{
+
+/**
+ * Simple installer for mods. Simple installers only deal with an in-memory structure
+ * representing the archive and can modify what to install where by editing this
+ * structure. Actually extracting the archive is handled by the caller Plugins
+ * implementing this interface have to contain the following line in the class
+ * declaration: Q_Interfaces(IPlugin IPluginInstaller IPluginInstallerCustom)
+ */
+class IPluginInstallerSimple : public QObject, public IPluginInstaller
+{
+
+ Q_INTERFACES(IPluginInstaller)
+
+public:
+ /**
+ * install call for the simple mode. The installer only needs to restructure the tree
+ * parameter, the caller does the rest
+ * @param modName name of the mod to install. As an input parameter this is the
+ * suggested name (i.e. from meta data) The installer may change this parameter to
+ * rename the mod)
+ * @param tree in-memory representation of the archive content
+ * @param version version of the mod. May be empty if the version is not yet known.
+ * Can be updated if the plugin can determine the version
+ * @param nexusID id of the mod or -1 if unknown. May be updated if the plugin can
+ * determine the mod id
+ * @return the result of the installation process. If "ERROR_NOTATTEMPTED" is
+ * returned, further installers will work with the modified tree. This may be useful
+ * when implementing a sort of filter, but usually tree should remain unchanged in
+ * that case.
+ */
+ virtual EInstallResult install(GuessedValue<QString>& modName,
+ std::shared_ptr<IFileTree>& tree, QString& version,
+ int& nexusID) = 0;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginInstallerSimple,
+ "com.tannin.ModOrganizer.PluginInstallerSimple/1.0")
+
+#endif // IPLUGININSTALLERSIMPLE_H
diff --git a/libs/uibase/include/uibase/ipluginlist.h b/libs/uibase/include/uibase/ipluginlist.h
new file mode 100644
index 0000000..61f8f7d
--- /dev/null
+++ b/libs/uibase/include/uibase/ipluginlist.h
@@ -0,0 +1,268 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2013 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 IPLUGINLIST_H
+#define IPLUGINLIST_H
+
+#include <QFlags>
+
+class QString;
+
+#include <functional>
+#include <map>
+
+namespace MOBase
+{
+
+class IPluginList
+{
+
+public:
+ enum PluginState
+ {
+ STATE_MISSING,
+ STATE_INACTIVE,
+ STATE_ACTIVE
+ };
+
+ Q_DECLARE_FLAGS(PluginStates, PluginState)
+
+public:
+ virtual ~IPluginList() {}
+
+ /**
+ * @return list of plugin names
+ */
+ virtual QStringList pluginNames() const = 0;
+
+ /**
+ * @brief retrieve the state of a plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return one of the possible plugin states: missing, inactive or active
+ */
+ virtual PluginStates state(const QString& name) const = 0;
+
+ /**
+ * @brief set the state of a plugin
+ * @param name filename of the plugin (without path but with file extensions)
+ * @param state new state of the plugin. should be active or inactive
+ */
+ virtual void setState(const QString& name, PluginStates state) = 0;
+
+ /**
+ * @brief retrieve the priority of a plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return the priority (the higher the more important). Returns -1 if the plugin
+ * doesn't exist
+ */
+ virtual int priority(const QString& name) const = 0;
+
+ /**
+ * @brief Change the priority of a plugin.
+ *
+ * @param name Filename of the plugin (without path but with file extension).
+ * @param newPriority New priority of the plugin.
+ *
+ * @return true on success, false if the priority change was not possible. This is
+ * usually because one of the parameters is invalid. The function returns true even if
+ * the plugin was not moved at the specified priority (e.g. when trying to move a
+ * non-master plugin before a master one).
+ */
+ virtual bool setPriority(const QString& name, int newPriority) = 0;
+
+ /**
+ * @brief retrieve the load order of a plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return the load order of a plugin (the order in which the game loads it). If all
+ * plugins are enabled this is the same as the priority but disabled plugins will have
+ * a load order of -1. This also returns -1 if the plugin doesn't exist
+ */
+ virtual int loadOrder(const QString& name) const = 0;
+
+ /**
+ * @brief sets the load order of the plugin list.
+ * @param the new load order, specified by the list of plugin names, sorted.
+ * @note plugins not included in the list will be placed at highest priority
+ * in the order they were before
+ */
+ virtual void setLoadOrder(const QStringList& pluginList) = 0;
+
+ /**
+ * @brief determine if a plugin is a master file (basically a library, referenced by
+ * other plugins)
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file is a master, false if it isn't OR if the file doesn't
+ * exist.
+ * @note deprecated
+ */
+ [[deprecated]] virtual bool isMaster(const QString& name) const = 0;
+
+ /**
+ * @brief retrieve the list of masters required for this plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return list of masters (filenames with extension, no path)
+ */
+ virtual QStringList masters(const QString& name) const = 0;
+
+ /**
+ * @brief retrieve the name of the origin of a plugin. This is either the (internal!)
+ * name of a mod or "overwrite" or "data"
+ * @param name filename of the plugin (without path but with file extension)
+ * @return name of the origin or an empty string if the plugin doesn't exist
+ * @note the internal name of a mod can differ from the display name for
+ * disambiguation
+ */
+ virtual QString origin(const QString& name) const = 0;
+
+ /**
+ * @brief invoked whenever the application felt it necessary to refresh the list (i.e.
+ * because of external changes)
+ */
+ virtual bool onRefreshed(const std::function<void()>& callback) = 0;
+
+ /**
+ * @brief invoked whenever a plugin has changed priority
+ */
+ virtual bool
+ onPluginMoved(const std::function<void(const QString&, int, int)>& func) = 0;
+
+ /**
+ * @brief Installs a handler for the event that the state of plugin changed
+ * (active/inactive).
+ *
+ * Parameters of the callback:
+ * - Map containing the plugins whose states have changed. Keys are plugin names and
+ * values are mod states.
+ *
+ * @param func The signal to be called when the states of any plugins change.
+ *
+ * @return true if the handler was successfully installed, false otherwise (there is
+ * as of now no known reason this should fail).
+ */
+ virtual bool onPluginStateChanged(
+ const std::function<void(const std::map<QString, PluginStates>&)>& func) = 0;
+
+ /**
+ * @brief determine if a plugin has the .esm extension (basically a library,
+ * referenced by other plugins)
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file has the .esm extension, false if it isn't OR if the file
+ * doesn't exist.
+ * @note in gamebryo games, a master file will usually have a .esm file
+ * extension but technically an esp can be flagged as master and an esm might
+ * not be
+ */
+ virtual bool hasMasterExtension(const QString& name) const = 0;
+
+ /**
+ * @brief determine if a plugin has the .esl extension
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file has the .esl extension, false if it isn't OR if the file
+ * doesn't exist.
+ * @note in gamebryo games, a light file will usually have a .esl file
+ * extension but technically an esp can be flagged as light and an esm might
+ * not be
+ */
+ virtual bool hasLightExtension(const QString& name) const = 0;
+
+ /**
+ * @brief determine if a plugin is flagged as master (basically a library, referenced
+ * by other plugins)
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file is flagged as master, false if it isn't OR if the file
+ * doesn't exist.
+ * @note in gamebryo games, a master file will usually have a .esm file
+ * extension but technically an esp can be flagged as master and an esm might
+ * not be
+ */
+ virtual bool isMasterFlagged(const QString& name) const = 0;
+
+ /**
+ * @brief determine if a plugin is flagged as medium
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file is flagged as medium, false if it isn't OR if the file
+ * doesn't exist.
+ * @note this plugin flag was added in Starfield and signifies plugins in between
+ * master and light plugins.
+ */
+ virtual bool isMediumFlagged(const QString& name) const = 0;
+
+ /**
+ * @brief determine if a plugin is flagged as light
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file is flagged as light, false if it isn't OR if the file
+ * doesn't exist.
+ * @note in gamebryo games, a light file will usually have a .esl file
+ */
+ virtual bool isLightFlagged(const QString& name) const = 0;
+
+ /**
+ * @brief determine if a plugin is flagged as blueprint
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file is flagged as blueprint, false if it isn't OR if the file
+ * doesn't exist.
+ * @note this plugin flag was added in Starfield and signifies plugins that are
+ * hidden in the Creation Kit and removed from Plugins.txt when the game loads a save
+ */
+ virtual bool isBlueprintFlagged(const QString& name) const = 0;
+
+ /**
+ * @brief determine if a plugin has no records
+ * @param name filename of the plugin (without path but with file extension)
+ * @return true if the file has no records, false if it does OR if the file doesn't
+ * exist.
+ */
+ virtual bool hasNoRecords(const QString& name) const = 0;
+
+ /**
+ * @brief retrieve the form version of a plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return the form version of the plugin, 0 if it doesn't have a form version or -1
+ * if the plugin doesn't exist
+ * @note Oblivion-style plugin headers don't have a form version
+ */
+ virtual int formVersion(const QString& name) const = 0;
+
+ /**
+ * @brief retrieve the header version of a plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return the header version of the plugin or -1 if the plugin doesn't exist
+ */
+ virtual float headerVersion(const QString& name) const = 0;
+
+ /**
+ * @brief retrieve the author of a plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return the author of the plugin or an empty string if the plugin doesn't exist
+ */
+ virtual QString author(const QString& name) const = 0;
+
+ /**
+ * @brief retrieve the description of a plugin
+ * @param name filename of the plugin (without path but with file extension)
+ * @return the description of the plugin or an empty string if the plugin doesn't
+ * exist
+ */
+ virtual QString description(const QString& name) const = 0;
+};
+
+} // namespace MOBase
+
+#endif // IPLUGINLIST_H
diff --git a/libs/uibase/include/uibase/ipluginmodpage.h b/libs/uibase/include/uibase/ipluginmodpage.h
new file mode 100644
index 0000000..78630d9
--- /dev/null
+++ b/libs/uibase/include/uibase/ipluginmodpage.h
@@ -0,0 +1,92 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2013 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 IPLUGINMODPAGE_H
+#define IPLUGINMODPAGE_H
+
+#include "iplugin.h"
+#include "modrepositoryfileinfo.h"
+#include <QIcon>
+
+namespace MOBase
+{
+
+class IPluginModPage : public QObject, public IPlugin
+{
+ Q_INTERFACES(IPlugin)
+public:
+ /**
+ * @return name of the Page as displayed in the ui
+ */
+ virtual QString displayName() const = 0;
+
+ /**
+ * @return icon to be displayed with the page
+ */
+ virtual QIcon icon() const = 0;
+
+ /**
+ * @return url to open when the user wants to visit this mod page
+ */
+ virtual QUrl pageURL() const = 0;
+
+ /**
+ * @return true if the page should be opened in the integrated browser
+ * @note unless the page provides a special means of starting downloads (like the
+ * nxm:// url schema on nexus) it will not be possible to handle downloads unless the
+ * integrated browser is used!
+ */
+ virtual bool useIntegratedBrowser() const = 0;
+
+ /**
+ * @brief test if the plugin handles a download
+ * @param pageURL url of the page that contained the donwload link
+ * @param downloadURL the actual download link
+ * @param fileInfo if the plugin can derive information from the urls it can store
+ * them here and they will be passed along with the download and be made available on
+ * installation
+ * @return true if this plugin wants to handle the specified download
+ */
+ virtual bool handlesDownload(const QUrl& pageURL, const QUrl& downloadURL,
+ ModRepositoryFileInfo& fileInfo) const = 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
+ */
+ virtual void setParentWidget(QWidget* widget) { m_ParentWidget = widget; }
+
+protected:
+ /**
+ * @brief getter for the parent widget
+ * @return parent widget
+ */
+ QWidget* parentWidget() const { return m_ParentWidget; }
+
+private:
+ QWidget* m_ParentWidget;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginModPage, "com.tannin.ModOrganizer.PluginModPage/1.0")
+
+#endif // IPLUGINMODPAGE_H
diff --git a/libs/uibase/include/uibase/ipluginpreview.h b/libs/uibase/include/uibase/ipluginpreview.h
new file mode 100644
index 0000000..47ace79
--- /dev/null
+++ b/libs/uibase/include/uibase/ipluginpreview.h
@@ -0,0 +1,72 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2014 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 IPLUGINPREVIEW_H
+#define IPLUGINPREVIEW_H
+
+#include "iplugin.h"
+
+namespace MOBase
+{
+
+class IPluginPreview : public QObject, public IPlugin
+{
+ Q_INTERFACES(IPlugin)
+public:
+ /**
+ * @return returns a set of file extensions that may be supported
+ */
+ virtual std::set<QString> supportedExtensions() const = 0;
+
+ /**
+ * @return return whether or not the preview supports raw data previews (likely
+ * sourced from an archive)
+ */
+ virtual bool supportsArchives() const { return false; }
+
+ /**
+ * @brief generate a preview widget for the specified file
+ * @param fileName name of the file to preview
+ * @param maxSize maximum size of the generated widget
+ * @return a widget showing the file
+ */
+ virtual QWidget* genFilePreview(const QString& fileName,
+ const QSize& maxSize) const = 0;
+
+ /**
+ * @brief generate a preview widget from an archive file loaded into memory
+ * @param fileData data of file to preview
+ * @param fileName path of file in virtual filesystem
+ * @param maxSize maxiumum size of the generated widget
+ */
+ virtual QWidget* genDataPreview(const QByteArray& fileData, const QString& fileName,
+ const QSize& maxSize) const
+ {
+ return nullptr;
+ }
+
+private:
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginPreview, "com.tannin.ModOrganizer.PluginPreview/1.0")
+
+#endif // IPLUGINPREVIEW_H
diff --git a/libs/uibase/include/uibase/ipluginproxy.h b/libs/uibase/include/uibase/ipluginproxy.h
new file mode 100644
index 0000000..b0b8d84
--- /dev/null
+++ b/libs/uibase/include/uibase/ipluginproxy.h
@@ -0,0 +1,83 @@
+/*
+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 <QDir>
+#include <QList>
+#include <QString>
+
+#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<QObject*> 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/libs/uibase/include/uibase/iplugintool.h b/libs/uibase/include/uibase/iplugintool.h
new file mode 100644
index 0000000..303ab36
--- /dev/null
+++ b/libs/uibase/include/uibase/iplugintool.h
@@ -0,0 +1,87 @@
+/*
+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 IPLUGINTOOL_H
+#define IPLUGINTOOL_H
+
+#include "iplugin.h"
+#include <QIcon>
+
+namespace MOBase
+{
+
+class IPluginTool : public QObject, public virtual IPlugin
+{
+ Q_INTERFACES(IPlugin)
+public:
+ IPluginTool() : m_ParentWidget(nullptr) {}
+
+ /**
+ * @return name of the tool as displayed in the ui
+ */
+ virtual QString displayName() const = 0;
+
+ /**
+ * @brief For IPluginTool, this returns displayName().
+ *
+ */
+ virtual QString localizedName() const { return displayName(); }
+
+ /**
+ * @return tooltip string
+ */
+
+ virtual QString tooltip() const = 0;
+ /**
+ * @return icon to be displayed with the tool
+ */
+ virtual QIcon icon() const = 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
+ */
+ virtual void setParentWidget(QWidget* widget) { m_ParentWidget = widget; }
+
+public Q_SLOTS:
+
+ /**
+ * @brief called when the user clicks to start the tool.
+ * @note This must not throw an exception!
+ */
+ virtual void display() const = 0;
+
+protected:
+ /**
+ * @brief getter for the parent widget
+ * @return parent widget
+ */
+ QWidget* parentWidget() const { return m_ParentWidget; }
+
+private:
+ QWidget* m_ParentWidget;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_INTERFACE(MOBase::IPluginTool, "com.tannin.ModOrganizer.PluginTool/1.0")
+
+#endif // IPLUGINTOOL_H
diff --git a/libs/uibase/include/uibase/iprofile.h b/libs/uibase/include/uibase/iprofile.h
new file mode 100644
index 0000000..1706e3e
--- /dev/null
+++ b/libs/uibase/include/uibase/iprofile.h
@@ -0,0 +1,41 @@
+#ifndef IPROFILE
+#define IPROFILE
+
+#include <QList>
+#include <QString>
+
+namespace MOBase
+{
+
+class IProfile
+{
+
+public:
+ virtual ~IProfile() {}
+
+ virtual QString name() const = 0;
+ virtual QString absolutePath() const = 0;
+ virtual bool localSavesEnabled() const = 0;
+ virtual bool localSettingsEnabled() const = 0;
+ virtual bool invalidationActive(bool* supported) const = 0;
+
+ /**
+ * @brief Retrieve the absolute file path to the corresponding INI file for this
+ * profile.
+ *
+ * @param iniFile INI file to retrieve a path for. This can either be the
+ * name of a file or a path to the absolute file outside of the profile.
+ *
+ * @return the absolute path for the given INI file for this profile.
+ *
+ * @note If iniFile does not correspond to a file in the list of INI files for the
+ * current game (as returned by IPluginGame::iniFiles), the path to the global
+ * file will be returned (if iniFile is absolute, iniFile is returned, otherwiise
+ the path is assumed relative to the game documents directory).
+ */
+ virtual QString absoluteIniFilePath(QString iniFile) const = 0;
+};
+
+} // namespace MOBase
+
+#endif // IPROFILE
diff --git a/libs/uibase/include/uibase/isavegame.h b/libs/uibase/include/uibase/isavegame.h
new file mode 100644
index 0000000..2f69446
--- /dev/null
+++ b/libs/uibase/include/uibase/isavegame.h
@@ -0,0 +1,58 @@
+#ifndef ISAVEGAMEINFO_H
+#define ISAVEGAMEINFO_H
+
+#include <QMetaType>
+
+class QString;
+class QDateTime;
+
+namespace MOBase
+{
+
+/** Base class for information about what is in a save game */
+class ISaveGame
+{
+public:
+ virtual ~ISaveGame() {}
+
+ /**
+ * @return the path of the (main) save file, either as an absolute file or
+ * relative to the save folder for which this save was created.
+ */
+ virtual QString getFilepath() const = 0;
+
+ /**
+ * @brief Retrieve the creation time of the save.
+ *
+ * Note that this might not be the same as the creation time of the file.
+ */
+ virtual QDateTime getCreationTime() const = 0;
+
+ /**
+ * @brief Retrieve the name of this save.
+ *
+ * @return the name of this save.
+ */
+ virtual QString getName() const = 0;
+
+ /**
+ * @brief Get a name which can be used to identify sets of saves.
+ *
+ * This is usually the PC name for RPG games. The name can contain '/' that
+ * are considered separate section for better visualization (not yet implemented).
+ */
+ virtual QString getSaveGroupIdentifier() const = 0;
+
+ /**
+ * @brief Gets all the files related to this save
+ *
+ * Note: This must return the actual list, not the potential list.
+ */
+ virtual QStringList allFiles() const = 0;
+};
+
+} // namespace MOBase
+
+Q_DECLARE_METATYPE(MOBase::ISaveGame const*)
+
+#endif // SAVEGAMEINFO_H
diff --git a/libs/uibase/include/uibase/isavegameinfowidget.h b/libs/uibase/include/uibase/isavegameinfowidget.h
new file mode 100644
index 0000000..d0620b0
--- /dev/null
+++ b/libs/uibase/include/uibase/isavegameinfowidget.h
@@ -0,0 +1,30 @@
+#ifndef ISAVEGAMEINFOWIDGET_H
+#define ISAVEGAMEINFOWIDGET_H
+
+#include <QWidget>
+
+#include "isavegame.h"
+
+class QFile;
+
+namespace MOBase
+{
+
+/** Base class for a save game info widget.
+ *
+ * This supports something or other
+ */
+class ISaveGameInfoWidget : public QWidget
+{
+public:
+ ISaveGameInfoWidget(QWidget* parent = 0) : QWidget(parent) {}
+
+ virtual ~ISaveGameInfoWidget() {}
+
+ /** Set the save file to display in the widget */
+ virtual void setSave(ISaveGame const&) = 0;
+};
+
+} // namespace MOBase
+
+#endif // ISAVEGAMEINFOWIDGET_H
diff --git a/libs/uibase/include/uibase/json.h b/libs/uibase/include/uibase/json.h
new file mode 100644
index 0000000..ad41bbc
--- /dev/null
+++ b/libs/uibase/include/uibase/json.h
@@ -0,0 +1,95 @@
+/**
+ * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and
+ * vice-versa. Copyright (C) 2011 Eeli Reilin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * \file json.h
+ */
+
+#ifndef JSON_H
+#define JSON_H
+
+#include <QList>
+#include <QString>
+#include <QVariant>
+
+/**
+ * \namespace QtJson
+ * \brief A JSON data parser
+ *
+ * Json parses a JSON data into a QVariant hierarchy.
+ */
+namespace QtJson
+{
+typedef QVariantMap JsonObject;
+typedef QVariantList JsonArray;
+
+/**
+ * Parse a JSON string
+ *
+ * \param json The JSON data
+ */
+QVariant parse(const QString& json);
+
+/**
+ * Parse a JSON string
+ *
+ * \param json The JSON data
+ * \param success The success of the parsing
+ */
+QVariant parse(const QString& json, bool& success);
+
+/**
+ * This method generates a textual JSON representation
+ *
+ * \param data The JSON data generated by the parser.
+ *
+ * \return QByteArray Textual JSON representation in UTF-8
+ */
+QByteArray serialize(const QVariant& data);
+
+/**
+ * This method generates a textual JSON representation
+ *
+ * \param data The JSON data generated by the parser.
+ * \param success The success of the serialization
+ *
+ * \return QByteArray Textual JSON representation in UTF-8
+ */
+QByteArray serialize(const QVariant& data, bool& success);
+
+/**
+ * This method generates a textual JSON representation
+ *
+ * \param data The JSON data generated by the parser.
+ *
+ * \return QString Textual JSON representation
+ */
+QString serializeStr(const QVariant& data);
+
+/**
+ * This method generates a textual JSON representation
+ *
+ * \param data The JSON data generated by the parser.
+ * \param success The success of the serialization
+ *
+ * \return QString Textual JSON representation
+ */
+QString serializeStr(const QVariant& data, bool& success);
+} // namespace QtJson
+
+#endif // JSON_H
diff --git a/libs/uibase/include/uibase/lineeditclear.h b/libs/uibase/include/uibase/lineeditclear.h
new file mode 100644
index 0000000..dfded64
--- /dev/null
+++ b/libs/uibase/include/uibase/lineeditclear.h
@@ -0,0 +1,40 @@
+/****************************************************************************
+**
+** Copyright (c) 2007 Trolltech ASA <info@trolltech.com>
+**
+** Use, modification and distribution is allowed without limitation,
+** warranty, liability or support of any kind.
+**
+****************************************************************************/
+
+#ifndef LINEEDITCLEAR_H
+#define LINEEDITCLEAR_H
+
+#include "dllimport.h"
+#include <QLineEdit>
+
+class QToolButton;
+
+namespace MOBase
+{
+
+class QDLLEXPORT LineEditClear : public QLineEdit
+{
+ Q_OBJECT
+
+public:
+ LineEditClear(QWidget* parent = 0);
+
+protected:
+ void resizeEvent(QResizeEvent*);
+
+private slots:
+ void updateCloseButton(const QString& text);
+
+private:
+ QToolButton* clearButton;
+};
+
+} // namespace MOBase
+
+#endif // LINEEDITCLEAR_H
diff --git a/libs/uibase/include/uibase/linklabel.h b/libs/uibase/include/uibase/linklabel.h
new file mode 100644
index 0000000..80fa958
--- /dev/null
+++ b/libs/uibase/include/uibase/linklabel.h
@@ -0,0 +1,38 @@
+#ifndef UIBASE_LINKLABEL_INCLUDED
+#define UIBASE_LINKLABEL_INCLUDED
+
+#include "dllimport.h"
+#include <QLabel>
+
+// this is a hack to allow .qss files to change the link color
+//
+// there's nothing in qt to change link colors from a qss file and the color
+// can't even be changed on individual widgets, they all use the _global_
+// palette on qApp
+//
+// so as soon as there's a LinkLabel present on screen, the global link color
+// will be set to whatever's in the qss file for:
+//
+// LinkLabel { qproperty-linkColor: cssColor; }
+//
+// this doesn't work for links that are visible, so changing the qss live won't
+// change the colors for those, MO has to be restarted
+//
+// apart from that, `LinkLabel` is just a `QLabel`
+//
+class QDLLEXPORT LinkLabel : public QLabel
+{
+ Q_OBJECT;
+ Q_PROPERTY(QColor linkColor READ linkColor WRITE setLinkColor);
+
+public:
+ LinkLabel(QWidget* parent = nullptr);
+
+ QColor linkColor() const;
+ void setLinkColor(const QColor& c);
+
+private:
+ static QColor m_linkColor;
+};
+
+#endif // UIBASE_LINKLABEL_INCLUDED
diff --git a/libs/uibase/include/uibase/log.h b/libs/uibase/include/uibase/log.h
new file mode 100644
index 0000000..1049506
--- /dev/null
+++ b/libs/uibase/include/uibase/log.h
@@ -0,0 +1,346 @@
+#pragma once
+
+#include <QColor>
+#include <QFlag>
+#include <QFlags>
+#include <QList>
+#include <QRect>
+#include <QSize>
+#include <QString>
+#include <QStringView>
+#include <filesystem>
+#include <string>
+#include <vector>
+
+#include <format>
+
+#include "dllimport.h"
+#include "formatters.h"
+#include <uibase/strings.h>
+
+namespace spdlog
+{
+class logger;
+}
+namespace spdlog::sinks
+{
+class sink;
+}
+
+namespace MOBase::log
+{
+
+enum Levels
+{
+ Debug = 0,
+ Info = 1,
+ Warning = 2,
+ Error = 3
+};
+
+struct BlacklistEntry
+{
+ std::string filter;
+ std::string replacement;
+};
+
+} // namespace MOBase::log
+
+namespace MOBase::log::details
+{
+
+// TODO: remove for C++23
+template <typename T>
+concept formattable = requires(T& v, std::format_context ctx) {
+ std::formatter<std::remove_cvref_t<T>>().format(v, ctx);
+};
+
+template <typename F, typename... Args>
+concept RuntimeFormatString = requires(F&& f, Args&&... args) {
+ (formattable<Args> && ...);
+ !std::is_convertible_v<std::decay_t<F>, std::string_view>;
+};
+
+void QDLLEXPORT doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept;
+
+template <class... Args>
+void doLog(spdlog::logger& logger, Levels lv,
+ const std::vector<MOBase::log::BlacklistEntry> bl,
+ std::format_string<Args...> format, Args&&... args) noexcept
+{
+ // format errors are logged without much information to avoid throwing again
+
+ std::string s;
+ try {
+ s = std::format(format, std::forward<Args>(args)...);
+
+ // check the blacklist
+ for (const BlacklistEntry& entry : bl) {
+ MOBase::ireplace_all(s, entry.filter, entry.replacement);
+ }
+ } catch (std::format_error&) {
+ s = "format error while logging";
+ lv = Levels::Error;
+ } catch (std::exception&) {
+ s = "exception while formatting for logging";
+ lv = Levels::Error;
+ } catch (...) {
+ s = "unknown exception while formatting for logging";
+ lv = Levels::Error;
+ }
+
+ doLogImpl(logger, lv, s);
+}
+
+template <class F, class... Args>
+void doLog(spdlog::logger& logger, Levels lv,
+ const std::vector<MOBase::log::BlacklistEntry> bl, F&& format,
+ Args&&... args) noexcept
+{
+ std::string s;
+
+ // format errors are logged without much information to avoid throwing again
+
+ try {
+ if constexpr (sizeof...(Args) == 0) {
+ s = std::format("{}", std::forward<F>(format));
+ } else if constexpr (std::is_same_v<std::decay_t<F>, QString>) {
+ s = std::vformat(format.toStdString(), std::make_format_args(args...));
+ } else {
+ s = std::vformat(std::forward<F>(format), std::make_format_args(args...));
+ }
+
+ // check the blacklist
+ for (const BlacklistEntry& entry : bl) {
+ MOBase::ireplace_all(s, entry.filter, entry.replacement);
+ }
+ } catch (std::format_error&) {
+ s = "format error while logging";
+ lv = Levels::Error;
+ } catch (std::exception&) {
+ s = "exception while formatting for logging";
+ lv = Levels::Error;
+ } catch (...) {
+ s = "unknown exception while formatting for logging";
+ lv = Levels::Error;
+ }
+
+ doLogImpl(logger, lv, s);
+}
+
+} // namespace MOBase::log::details
+
+namespace MOBase::log
+{
+
+struct QDLLEXPORT File
+{
+public:
+ enum Types
+ {
+ None = 0,
+ Daily,
+ Rotating,
+ Single
+ };
+
+ File();
+
+ static File daily(std::filesystem::path file, int hour, int minute);
+
+ static File rotating(std::filesystem::path file, std::size_t maxSize,
+ std::size_t maxFiles);
+
+ static File single(std::filesystem::path file);
+
+ Types type;
+ std::filesystem::path file;
+ std::size_t maxSize, maxFiles;
+ int dailyHour, dailyMinute;
+};
+
+struct Entry
+{
+ std::chrono::system_clock::time_point time;
+ Levels level;
+ std::string message;
+ std::string formattedMessage;
+};
+
+using Callback = void(Entry);
+
+struct LoggerConfiguration
+{
+ std::string name;
+ Levels maxLevel = Levels::Info;
+ std::string pattern;
+ bool utc = false;
+ std::vector<BlacklistEntry> blacklist;
+};
+
+class QDLLEXPORT Logger
+{
+public:
+ Logger(LoggerConfiguration conf);
+ ~Logger();
+
+ Levels level() const;
+ void setLevel(Levels lv);
+
+ void setPattern(const std::string& pattern);
+ void setFile(const File& f);
+ void setCallback(Callback* f);
+
+ void addToBlacklist(const std::string& filter, const std::string& replacement);
+ void removeFromBlacklist(const std::string& filter);
+ void resetBlacklist();
+
+ template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+ void debug(F&& format, Args&&... args) noexcept
+ {
+ log(Debug, std::forward<F>(format), std::forward<Args>(args)...);
+ }
+
+ template <class... Args>
+ void debug(std::format_string<Args...> format, Args&&... args) noexcept
+ {
+ log(Debug, format, std::forward<Args>(args)...);
+ }
+
+ template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+ void info(F&& format, Args&&... args) noexcept
+ {
+ log(Info, std::forward<F>(format), std::forward<Args>(args)...);
+ }
+
+ template <class... Args>
+ void info(std::format_string<Args...> format, Args&&... args) noexcept
+ {
+ log(Info, format, std::forward<Args>(args)...);
+ }
+
+ template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+ void warn(F&& format, Args&&... args) noexcept
+ {
+ log(Warning, std::forward<F>(format), std::forward<Args>(args)...);
+ }
+
+ template <class... Args>
+ void warn(std::format_string<Args...> format, Args&&... args) noexcept
+ {
+ log(Warning, format, std::forward<Args>(args)...);
+ }
+
+ template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+ void error(F&& format, Args&&... args) noexcept
+ {
+ log(Error, std::forward<F>(format), std::forward<Args>(args)...);
+ }
+
+ template <class... Args>
+ void error(std::format_string<Args...> format, Args&&... args) noexcept
+ {
+ log(Error, format, std::forward<Args>(args)...);
+ }
+
+ template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+ void log(Levels lv, F&& format, Args&&... args) noexcept
+ {
+ details::doLog(*m_logger, lv, m_conf.blacklist, std::forward<F>(format),
+ std::forward<Args>(args)...);
+ }
+
+ template <class... Args>
+ void log(Levels lv, std::format_string<Args...> format, Args&&... args) noexcept
+ {
+ details::doLog(*m_logger, lv, m_conf.blacklist, format,
+ std::forward<Args>(args)...);
+ }
+
+private:
+ LoggerConfiguration m_conf;
+ std::unique_ptr<spdlog::logger> m_logger;
+ std::shared_ptr<spdlog::sinks::sink> m_sinks;
+ std::shared_ptr<spdlog::sinks::sink> m_console, m_callback, m_file;
+
+ void createLogger(const std::string& name);
+ void addSink(std::shared_ptr<spdlog::sinks::sink> sink);
+};
+
+QDLLEXPORT void createDefault(LoggerConfiguration conf);
+QDLLEXPORT Logger& getDefault();
+
+template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+void debug(F&& format, Args&&... args) noexcept
+{
+ getDefault().debug(std::forward<F>(format), std::forward<Args>(args)...);
+}
+
+template <class... Args>
+void debug(std::format_string<Args...> format, Args&&... args) noexcept
+{
+ getDefault().debug(format, std::forward<Args>(args)...);
+}
+
+template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+void info(F&& format, Args&&... args) noexcept
+{
+ getDefault().info(std::forward<F>(format), std::forward<Args>(args)...);
+}
+
+template <class... Args>
+void info(std::format_string<Args...> format, Args&&... args) noexcept
+{
+ getDefault().info(format, std::forward<Args>(args)...);
+}
+
+template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+void warn(F&& format, Args&&... args) noexcept
+{
+ getDefault().warn(std::forward<F>(format), std::forward<Args>(args)...);
+}
+
+template <class... Args>
+void warn(std::format_string<Args...> format, Args&&... args) noexcept
+{
+ getDefault().warn(format, std::forward<Args>(args)...);
+}
+
+template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+void error(F&& format, Args&&... args) noexcept
+{
+ getDefault().error(std::forward<F>(format), std::forward<Args>(args)...);
+}
+
+template <class... Args>
+void error(std::format_string<Args...> format, Args&&... args) noexcept
+{
+ getDefault().error(format, std::forward<Args>(args)...);
+}
+
+template <class F, class... Args>
+ requires(details::RuntimeFormatString<F, Args...>)
+void log(Levels lv, F&& format, Args&&... args) noexcept
+{
+ getDefault().log(lv, std::forward<F>(format), std::forward<Args>(args)...);
+}
+
+template <class... Args>
+void log(Levels lv, std::format_string<Args...> format, Args&&... args) noexcept
+{
+ getDefault().log(lv, format, std::forward<Args>(args)...);
+}
+
+//
+QDLLEXPORT QString levelToString(Levels level);
+
+} // namespace MOBase::log
diff --git a/libs/uibase/include/uibase/memoizedlock.h b/libs/uibase/include/uibase/memoizedlock.h
new file mode 100644
index 0000000..1888484
--- /dev/null
+++ b/libs/uibase/include/uibase/memoizedlock.h
@@ -0,0 +1,61 @@
+#ifndef MO_UIBASE_MEMOIZEDLOCK_INCLUDED
+#define MO_UIBASE_MEMOIZEDLOCK_INCLUDED
+
+// Do not put this in utility.h otherwise the C# projects will
+// fail to compile since apparently <mutex> is not available in
+// C++/CLI projects.
+
+#include <functional>
+#include <mutex>
+
+namespace MOBase
+{
+
+/**
+ * Class that can be used to perform thread-safe memoization.
+ *
+ * Each instance hold a flag indicating if the current value is up-to-date
+ * or not. This flag can be reset using `invalidate()`. When the value is queried,
+ * the flag is checked, and if it is not up-to-date, the given callback is used
+ * to compute the value.
+ *
+ * The computation and update of the value is locked to avoid concurrent modifications.
+ *
+ * @tparam T Type of value ot memoized.
+ * @tparam Fn Type of the callback.
+ */
+template <class T, class Fn = std::function<T()>>
+class MemoizedLocked
+{
+public:
+ template <class Callable>
+ MemoizedLocked(Callable&& callable, T value = {})
+ : m_Fn{std::forward<Callable>(callable)}, m_Value{std::move(value)}
+ {}
+
+ template <class... Args>
+ T& value(Args&&... args) const
+ {
+ if (m_NeedUpdating) {
+ std::scoped_lock lock(m_Mutex);
+ if (m_NeedUpdating) {
+ m_Value = std::invoke(m_Fn, std::forward<Args>(args)...);
+ m_NeedUpdating = false;
+ }
+ }
+ return m_Value;
+ }
+
+ void invalidate() { m_NeedUpdating = true; }
+
+private:
+ mutable std::mutex m_Mutex;
+ mutable std::atomic<bool> m_NeedUpdating{true};
+
+ Fn m_Fn;
+ mutable T m_Value;
+};
+
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/moassert.h b/libs/uibase/include/uibase/moassert.h
new file mode 100644
index 0000000..492381c
--- /dev/null
+++ b/libs/uibase/include/uibase/moassert.h
@@ -0,0 +1,36 @@
+#ifndef UIBASE_MOASSERT_INCLUDED
+#define UIBASE_MOASSERT_INCLUDED
+
+#include "log.h"
+#include <csignal>
+
+namespace MOBase
+{
+
+template <class T>
+inline void MOAssert(T&& t, const char* exp, const char* file, int line,
+ const char* func)
+{
+ if (!t) {
+ log::error("assertion failed: {}:{} {}: '{}'", file, line, func, exp);
+
+#ifdef _WIN32
+ if (IsDebuggerPresent()) {
+ DebugBreak();
+ }
+#else
+ // On Linux, raise SIGTRAP if a debugger might be attached
+ raise(SIGTRAP);
+#endif
+ }
+}
+
+} // namespace MOBase
+
+#ifdef _MSC_VER
+#define MO_ASSERT(v) MOBase::MOAssert(v, #v, __FILE__, __LINE__, __FUNCSIG__)
+#else
+#define MO_ASSERT(v) MOBase::MOAssert(v, #v, __FILE__, __LINE__, __PRETTY_FUNCTION__)
+#endif
+
+#endif // UIBASE_MOASSERT_INCLUDED
diff --git a/libs/uibase/include/uibase/modrepositoryfileinfo.h b/libs/uibase/include/uibase/modrepositoryfileinfo.h
new file mode 100644
index 0000000..bf41f72
--- /dev/null
+++ b/libs/uibase/include/uibase/modrepositoryfileinfo.h
@@ -0,0 +1,56 @@
+#ifndef MODREPOSITORYFILEINFO_H
+#define MODREPOSITORYFILEINFO_H
+
+#include "versioninfo.h"
+#include <QDateTime>
+#include <QString>
+#include <QVariantMap>
+
+namespace MOBase
+{
+enum EFileCategory
+{
+ TYPE_UNKNOWN = 0,
+ TYPE_MAIN,
+ TYPE_UPDATE,
+ TYPE_OPTION
+};
+
+class QDLLEXPORT ModRepositoryFileInfo : public QObject
+{
+ Q_OBJECT
+
+public:
+ ModRepositoryFileInfo(const ModRepositoryFileInfo& reference);
+ ModRepositoryFileInfo(QString gameName = "", int modID = 0, int fileID = 0);
+ QString toString() const;
+
+ static ModRepositoryFileInfo createFromJson(const QString& data);
+ QString name;
+ QString uri;
+ QString description;
+ VersionInfo version;
+ VersionInfo newestVersion;
+ int categoryID;
+ QString modName;
+ QString gameName;
+ QString nexusKey;
+ int modID;
+ int fileID;
+ int nexusExpires;
+ int nexusDownloadUser;
+ size_t fileSize;
+ QString fileName;
+ int fileCategory;
+ QDateTime fileTime;
+ QString repository;
+
+ QVariantMap userData;
+
+ QString author;
+ QString uploader;
+ QString uploaderUrl;
+};
+} // namespace MOBase
+
+#endif // MODREPOSITORYFILEINFO_H
diff --git a/libs/uibase/include/uibase/nxmurl.h b/libs/uibase/include/uibase/nxmurl.h
new file mode 100644
index 0000000..bd06d3d
--- /dev/null
+++ b/libs/uibase/include/uibase/nxmurl.h
@@ -0,0 +1,87 @@
+/*
+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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef NXMURL_H
+#define NXMURL_H
+
+#include "dllimport.h"
+#include <QList>
+#include <QObject>
+#include <QString>
+
+/**
+ * @brief represents a nxm:// url
+ * @todo the game name encoded into the url is not interpreted
+ **/
+class QDLLEXPORT NXMUrl : public QObject
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ *
+ * @param url url following the nxm-protocol
+ **/
+ NXMUrl(const QString& url);
+
+ /**
+ * @return name of the game
+ */
+ QString game() const { return m_Game; }
+
+ /**
+ * @return the NMX request key
+ */
+ QString key() const { return m_Key; }
+
+ /**
+ * @brief retrieve the mod id encoded into the url
+ *
+ * @return mod id
+ **/
+ int modId() const { return m_ModId; }
+
+ /**
+ * @brief retrieve the file id encoded into the url
+ *
+ * @return file id
+ **/
+ int fileId() const { return m_FileId; }
+
+ /**
+ * @return the expires timestamp
+ */
+ int expires() const { return m_Expires; }
+
+ /**
+ * @return the parsed user ID
+ */
+ int userId() const { return m_UserId; }
+
+private:
+ QString m_Game;
+ QString m_Key;
+ int m_ModId;
+ int m_FileId;
+ int m_Expires;
+ int m_UserId;
+};
+
+#endif // NXMURL_H
diff --git a/libs/uibase/include/uibase/pluginrequirements.h b/libs/uibase/include/uibase/pluginrequirements.h
new file mode 100644
index 0000000..ff3668a
--- /dev/null
+++ b/libs/uibase/include/uibase/pluginrequirements.h
@@ -0,0 +1,193 @@
+#ifndef PLUGINREQUIREMENTS_H
+#define PLUGINREQUIREMENTS_H
+
+#include <functional>
+#include <memory>
+#include <optional>
+
+#include <QList>
+#include <QString>
+
+#include "dllimport.h"
+
+namespace MOBase
+{
+
+class IOrganizer;
+class IPluginDiagnose;
+
+/**
+ * @brief The interface for plugin requirements.
+ */
+class IPluginRequirement
+{
+public:
+ class Problem
+ {
+ public:
+ /**
+ * @return a short description for the problem.
+ */
+ QString shortDescription() const { return m_ShortDescription; }
+
+ /**
+ * @return a long description for the problem.
+ */
+ QString longDescription() const { return m_LongDescription; }
+
+ /**
+ *
+ */
+ Problem(QString shortDescription, QString longDescription = "")
+ : m_ShortDescription(shortDescription),
+ m_LongDescription(longDescription.isEmpty() ? shortDescription
+ : longDescription)
+ {}
+
+ private:
+ QString m_ShortDescription, m_LongDescription;
+ };
+
+public:
+ /**
+ * @brief Check if the requirements is met.
+ *
+ * @param organizer The organizer proxy.
+ *
+ * @return a problem if the requirement is not met, otherwise an empty optional.
+ */
+ virtual std::optional<Problem> check(IOrganizer* organizer) const = 0;
+
+ virtual ~IPluginRequirement() {}
+};
+
+/**
+ * @brief Plugin dependency - The requirement is met if one of the
+ * given plugin is active.
+ */
+class QDLLEXPORT PluginDependencyRequirement : public IPluginRequirement
+{
+
+ friend class PluginRequirementFactory;
+
+public:
+ PluginDependencyRequirement(QStringList const& pluginNames);
+
+ virtual std::optional<Problem> check(IOrganizer* organizer) const;
+
+ /**
+ * @return the list of plugin names in this dependency requirement.
+ */
+ QStringList pluginNames() const { return m_PluginNames; }
+
+protected:
+ QString message() const;
+ QStringList m_PluginNames;
+};
+
+/**
+ * @brief Game dependency - The requirement is met if the active game
+ * is one of the specified game.
+ */
+class QDLLEXPORT GameDependencyRequirement : public IPluginRequirement
+{
+
+ friend class PluginRequirementFactory;
+
+public:
+ GameDependencyRequirement(QStringList const& gameNames);
+
+ virtual std::optional<Problem> check(IOrganizer* organizer) const;
+
+ /**
+ * @return the list of game names in this dependency requirement.
+ */
+ QStringList gameNames() const { return m_GameNames; }
+
+protected:
+ QString message() const;
+ QStringList m_GameNames;
+};
+
+/**
+ * @brief Diagnose dependency - This wrap a IPluginDiagnose into a plugin
+ * requirements.
+ *
+ * If the wrapped diagnose plugin reports a problem, the requirement fails
+ * and the associated message is the one from the diagnose plugin (or the
+ * list of messages if multiple problems were reported).
+ */
+class DiagnoseRequirement : public IPluginRequirement
+{
+
+ friend class PluginRequirementFactory;
+
+public:
+ DiagnoseRequirement(const IPluginDiagnose* diagnose);
+
+ virtual std::optional<Problem> check(IOrganizer* organizer) const;
+
+private:
+ const IPluginDiagnose* m_Diagnose;
+};
+
+/**
+ * Factory for plugin requirements.
+ */
+class QDLLEXPORT PluginRequirementFactory
+{
+public:
+ /**
+ * @brief Create a new plugin dependency. The requirement is met if one of the
+ * given plugin is enabled.
+ *
+ * If you want all plugins to be active, simply create multiple requirements.
+ *
+ * @param pluginNames Name of the plugin required.
+ */
+ static std::shared_ptr<const IPluginRequirement>
+ pluginDependency(QStringList const& pluginNames);
+ static std::shared_ptr<const IPluginRequirement>
+ pluginDependency(QString const& pluginName)
+ {
+ return pluginDependency(QStringList{pluginName});
+ }
+
+ /**
+ * @brief Create a new plugin dependency. The requirement is met if the current
+ * game plugin matches one of the given name.
+ *
+ * @param gameName Name of the game required.
+ *
+ * @note This differ from makePluginDependency only for the message.
+ */
+ static std::shared_ptr<const IPluginRequirement>
+ gameDependency(QStringList const& gameNames);
+ static std::shared_ptr<const IPluginRequirement>
+ gameDependency(QString const& gameName)
+ {
+ return gameDependency(QStringList{gameName});
+ }
+
+ /**
+ * @brief Create a requirement from the given diagnose plugin.
+ *
+ * @param diagnose The diagnose plugin.
+ */
+ static std::shared_ptr<const IPluginRequirement>
+ diagnose(const IPluginDiagnose* diagnose);
+
+ /**
+ * @brief Create a generic requirement with the given checker and message.
+ *
+ * @param checker The function to use to check if the requirement is met (should
+ * return true if the requirement is met).
+ * @param description The description to show user if the requirement is not met.
+ */
+ static std::shared_ptr<const IPluginRequirement>
+ basic(std::function<bool(IOrganizer*)> const& checker, QString const description);
+};
+
+} // namespace MOBase
+
+#endif
diff --git a/libs/uibase/include/uibase/pluginsetting.h b/libs/uibase/include/uibase/pluginsetting.h
new file mode 100644
index 0000000..6844f4d
--- /dev/null
+++ b/libs/uibase/include/uibase/pluginsetting.h
@@ -0,0 +1,50 @@
+/*
+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 PLUGINSETTING_H
+#define PLUGINSETTING_H
+
+#include <QList>
+#include <QString>
+#include <QVariant>
+
+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;
+};
+
+} // namespace MOBase
+
+#endif // PLUGINSETTING_H
diff --git a/libs/uibase/include/uibase/questionboxmemory.h b/libs/uibase/include/uibase/questionboxmemory.h
new file mode 100644
index 0000000..c5224ea
--- /dev/null
+++ b/libs/uibase/include/uibase/questionboxmemory.h
@@ -0,0 +1,112 @@
+/*
+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 QUESTIONBOXMEMORY_H
+#define QUESTIONBOXMEMORY_H
+
+#include "dllimport.h"
+
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QList>
+#include <QObject>
+#include <QString>
+
+class QAbstractButton;
+class QMutex;
+class QSettings;
+class QWidget;
+
+namespace Ui
+{
+class QuestionBoxMemory;
+}
+
+namespace MOBase
+{
+
+class QDLLEXPORT QuestionBoxMemory : public QDialog
+{
+ Q_OBJECT
+
+public:
+ using Button = QDialogButtonBox::StandardButton;
+ static const auto NoButton = QDialogButtonBox::NoButton;
+
+ using GetButton = std::function<Button(const QString&, const QString&)>;
+ using SetWindowButton = std::function<void(const QString&, Button)>;
+ using SetFileButton = std::function<void(const QString&, const QString&, Button)>;
+
+ ~QuestionBoxMemory();
+
+ // QuestionBoxMemory needs to access the settings, but they're only in
+ // the modorganizer project; the only way to avoid accessing the ini file
+ // directly is to use callbacks registered in Settings' constructor
+ //
+ static void setCallbacks(GetButton get, SetWindowButton setWindow,
+ SetFileButton setFile);
+
+ static Button
+ query(QWidget* parent, const QString& windowName, const QString& title,
+ const QString& text,
+ QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes |
+ QDialogButtonBox::No,
+ QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::NoButton);
+
+ static Button
+ query(QWidget* parent, const QString& windowName, const QString& fileName,
+ const QString& title, const QString& text,
+ QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes |
+ QDialogButtonBox::No,
+ QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::NoButton);
+
+ static void setWindowMemory(const QString& windowName, Button b);
+
+ static void setFileMemory(const QString& windowName, const QString& filename,
+ Button b);
+
+ static Button getMemory(const QString& windowName, const QString& filename);
+
+ static QString buttonToString(Button b);
+
+private slots:
+ void buttonClicked(QAbstractButton* button);
+
+private:
+ explicit QuestionBoxMemory(QWidget* parent, const QString& title, const QString& text,
+ const QString* filename,
+ const QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton);
+
+private:
+ static Button queryImpl(
+ QWidget* parent, const QString& windowName, const QString* fileName,
+ const QString& title, const QString& text,
+ QDialogButtonBox::StandardButtons buttons = QDialogButtonBox::Yes |
+ QDialogButtonBox::No,
+ QDialogButtonBox::StandardButton defaultButton = QDialogButtonBox::NoButton);
+
+ std::unique_ptr<Ui::QuestionBoxMemory> ui;
+ QDialogButtonBox::StandardButton m_Button;
+};
+
+} // namespace MOBase
+
+#endif // QUESTIONBOXMEMORY_H
diff --git a/libs/uibase/include/uibase/registry.h b/libs/uibase/include/uibase/registry.h
new file mode 100644
index 0000000..00013df
--- /dev/null
+++ b/libs/uibase/include/uibase/registry.h
@@ -0,0 +1,39 @@
+/*
+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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef REGISTRY_H
+#define REGISTRY_H
+
+#include "dllimport.h"
+#include <QString>
+
+namespace MOBase
+{
+
+// On Linux, writes INI values using QSettings instead of WritePrivateProfileString
+QDLLEXPORT bool WriteRegistryValue(const QString& appName, const QString& keyName,
+ const QString& value, const QString& fileName);
+
+#ifdef _WIN32
+// Windows-specific overload using wide strings
+QDLLEXPORT bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName,
+ const wchar_t* value, const wchar_t* fileName);
+#endif
+
+} // namespace MOBase
+
+#endif // REGISTRY_H
diff --git a/libs/uibase/include/uibase/report.h b/libs/uibase/include/uibase/report.h
new file mode 100644
index 0000000..f164681
--- /dev/null
+++ b/libs/uibase/include/uibase/report.h
@@ -0,0 +1,117 @@
+/*
+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 REPORT_H
+#define REPORT_H
+
+#include "dllimport.h"
+#include <QCheckBox>
+#include <QComboBox>
+#include <QList>
+#include <QMessageBox>
+#include <QPlainTextEdit>
+#include <wchar.h>
+
+namespace Ui
+{
+class TaskDialog;
+}
+
+namespace MOBase
+{
+
+class ExpanderWidget;
+
+// Convenience function displaying an error message box. This function uses
+// WinAPI if no Qt Window is available yet or QMessageBox otherwise.
+//
+QDLLEXPORT void reportError(const QString& message);
+
+// shows a critical message box that's raised to the top of the zorder, useful
+// for messages without a main window, which sometimes makes them pop up behind
+// all other windows
+//
+// the dialog is not topmost, it's just raised once when shown
+//
+QDLLEXPORT void criticalOnTop(const QString& message);
+
+struct QDLLEXPORT TaskDialogButton
+{
+ QString text, description;
+ QMessageBox::StandardButton button;
+
+ TaskDialogButton(QString text, QString description,
+ QMessageBox::StandardButton button);
+ TaskDialogButton(QString text, QMessageBox::StandardButton button);
+};
+
+class QDLLEXPORT TaskDialog
+{
+public:
+ TaskDialog(QWidget* parent = nullptr, QString title = {});
+ ~TaskDialog();
+
+ TaskDialog& title(const QString& s);
+ TaskDialog& main(const QString& s);
+ TaskDialog& content(const QString& s);
+ TaskDialog& details(const QString& s);
+ TaskDialog& icon(QMessageBox::Icon i);
+ TaskDialog& button(TaskDialogButton b);
+ TaskDialog& remember(const QString& action, const QString& file = {});
+
+ void addContent(QWidget* w);
+ void setWidth(int w);
+
+ QMessageBox::StandardButton exec();
+
+private:
+ std::unique_ptr<QDialog> m_dialog;
+ std::unique_ptr<Ui::TaskDialog> ui;
+ QString m_title, m_main, m_content, m_details;
+ QMessageBox::Icon m_icon;
+ std::vector<TaskDialogButton> m_buttons;
+ QMessageBox::StandardButton m_result;
+ std::unique_ptr<ExpanderWidget> m_expander;
+ int m_width;
+
+ QString m_rememberAction, m_rememberFile;
+ QCheckBox* m_rememberCheck;
+ QComboBox* m_rememberCombo;
+
+ QMessageBox::StandardButton checkMemory() const;
+ void rememberChoice();
+
+ void setDialog();
+ void setWidgets();
+ void setChoices();
+ void setButtons();
+ void setStandardButtons();
+ void setCommandButtons();
+ void setDetails();
+
+ QColor detailsColor() const;
+ QPixmap standardIcon(QMessageBox::Icon icon) const;
+ void setVisibleLines(QPlainTextEdit* w, int lines);
+ void setFontPercent(QWidget* w, double p);
+};
+
+} // namespace MOBase
+
+#endif // REPORT_H
diff --git a/libs/uibase/include/uibase/safewritefile.h b/libs/uibase/include/uibase/safewritefile.h
new file mode 100644
index 0000000..3ce9576
--- /dev/null
+++ b/libs/uibase/include/uibase/safewritefile.h
@@ -0,0 +1,72 @@
+/*
+Copyright (C) 2014 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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef SAFEWRITEFILE_H
+#define SAFEWRITEFILE_H
+
+#include "dllimport.h"
+#include "utility.h"
+#include <QFile>
+#include <QSaveFile>
+#include <QString>
+
+namespace MOBase
+{
+
+#ifndef _WIN32
+// Thin QFile wrapper that adds a no-op commit() for QSaveFile API compat.
+class DirectWriteFile : public QFile
+{
+public:
+ using QFile::QFile;
+ bool commit()
+ {
+ flush();
+ return true;
+ }
+};
+#endif
+
+/**
+ * @brief a wrapper for QSaveFile that handles errors when opening the file to reduce
+ * code duplication. On Linux, uses plain QFile because QSaveFile's temp-file
+ * strategy fails on many common filesystem configurations.
+ */
+class QDLLEXPORT SafeWriteFile
+{
+public:
+ SafeWriteFile(const QString& fileName);
+
+#ifdef _WIN32
+ QSaveFile* operator->();
+#else
+ DirectWriteFile* operator->();
+#endif
+
+private:
+#ifdef _WIN32
+ QSaveFile m_SaveFile;
+#else
+ DirectWriteFile m_File;
+#endif
+};
+
+} // namespace MOBase
+
+#endif // SAFEWRITEFILE_H
diff --git a/libs/uibase/include/uibase/scopeguard.h b/libs/uibase/include/uibase/scopeguard.h
new file mode 100644
index 0000000..11320f3
--- /dev/null
+++ b/libs/uibase/include/uibase/scopeguard.h
@@ -0,0 +1,296 @@
+#ifndef SCOPEGUARD_H
+#define SCOPEGUARD_H
+
+/*
+ Scopeguard, by Andrei Alexandrescu and Petru Marginean, December 2000.
+ Modified by Joshua Lehrer, FactSet Research Systems, November 2005.
+*/
+
+template <class T>
+class RefHolder
+{
+ T& ref_;
+
+public:
+ RefHolder(T& ref) : ref_(ref) {}
+ operator T&() const { return ref_; }
+
+private:
+ // Disable assignment - not implemented
+ RefHolder& operator=(const RefHolder&);
+};
+
+template <class T>
+inline RefHolder<T> ByRef(T& t)
+{
+ return RefHolder<T>(t);
+}
+
+class ScopeGuardImplBase
+{
+ ScopeGuardImplBase& operator=(const ScopeGuardImplBase&);
+
+protected:
+ ~ScopeGuardImplBase() {}
+ ScopeGuardImplBase(const ScopeGuardImplBase& other) throw()
+ : dismissed_(other.dismissed_)
+ {
+ other.Dismiss();
+ }
+ template <typename J>
+ static void SafeExecute(J& j) throw()
+ {
+ if (!j.dismissed_)
+ try {
+ j.Execute();
+ } catch (...) {
+ }
+ }
+
+ mutable bool dismissed_;
+
+public:
+ ScopeGuardImplBase() throw() : dismissed_(false) {}
+ void Dismiss() const throw() { dismissed_ = true; }
+};
+
+typedef const ScopeGuardImplBase& ScopeGuard;
+
+template <typename F>
+class ScopeGuardImpl0 : public ScopeGuardImplBase
+{
+public:
+ static ScopeGuardImpl0<F> MakeGuard(F fun) { return ScopeGuardImpl0<F>(fun); }
+ ~ScopeGuardImpl0() throw() { SafeExecute(*this); }
+ void Execute() { fun_(); }
+
+protected:
+ ScopeGuardImpl0(F fun) : fun_(fun) {}
+ F fun_;
+};
+
+template <typename F>
+inline ScopeGuardImpl0<F> MakeGuard(F fun)
+{
+ return ScopeGuardImpl0<F>::MakeGuard(fun);
+}
+
+template <typename F, typename P1>
+class ScopeGuardImpl1 : public ScopeGuardImplBase
+{
+public:
+ static ScopeGuardImpl1<F, P1> MakeGuard(F fun, P1 p1)
+ {
+ return ScopeGuardImpl1<F, P1>(fun, p1);
+ }
+ ~ScopeGuardImpl1() throw() { SafeExecute(*this); }
+ void Execute() { fun_(p1_); }
+
+protected:
+ ScopeGuardImpl1(F fun, P1 p1) : fun_(fun), p1_(p1) {}
+ F fun_;
+ const P1 p1_;
+};
+
+template <typename F, typename P1>
+inline ScopeGuardImpl1<F, P1> MakeGuard(F fun, P1 p1)
+{
+ return ScopeGuardImpl1<F, P1>::MakeGuard(fun, p1);
+}
+
+template <typename F, typename P1, typename P2>
+class ScopeGuardImpl2 : public ScopeGuardImplBase
+{
+public:
+ static ScopeGuardImpl2<F, P1, P2> MakeGuard(F fun, P1 p1, P2 p2)
+ {
+ return ScopeGuardImpl2<F, P1, P2>(fun, p1, p2);
+ }
+ ~ScopeGuardImpl2() throw() { SafeExecute(*this); }
+ void Execute() { fun_(p1_, p2_); }
+
+protected:
+ ScopeGuardImpl2(F fun, P1 p1, P2 p2) : fun_(fun), p1_(p1), p2_(p2) {}
+ F fun_;
+ const P1 p1_;
+ const P2 p2_;
+};
+
+template <typename F, typename P1, typename P2>
+inline ScopeGuardImpl2<F, P1, P2> MakeGuard(F fun, P1 p1, P2 p2)
+{
+ return ScopeGuardImpl2<F, P1, P2>::MakeGuard(fun, p1, p2);
+}
+
+template <typename F, typename P1, typename P2, typename P3>
+class ScopeGuardImpl3 : public ScopeGuardImplBase
+{
+public:
+ static ScopeGuardImpl3<F, P1, P2, P3> MakeGuard(F fun, P1 p1, P2 p2, P3 p3)
+ {
+ return ScopeGuardImpl3<F, P1, P2, P3>(fun, p1, p2, p3);
+ }
+ ~ScopeGuardImpl3() throw() { SafeExecute(*this); }
+ void Execute() { fun_(p1_, p2_, p3_); }
+
+protected:
+ ScopeGuardImpl3(F fun, P1 p1, P2 p2, P3 p3) : fun_(fun), p1_(p1), p2_(p2), p3_(p3) {}
+ F fun_;
+ const P1 p1_;
+ const P2 p2_;
+ const P3 p3_;
+};
+
+template <typename F, typename P1, typename P2, typename P3>
+inline ScopeGuardImpl3<F, P1, P2, P3> MakeGuard(F fun, P1 p1, P2 p2, P3 p3)
+{
+ return ScopeGuardImpl3<F, P1, P2, P3>::MakeGuard(fun, p1, p2, p3);
+}
+
+//************************************************************
+
+template <class Obj, typename MemFun>
+class ObjScopeGuardImpl0 : public ScopeGuardImplBase
+{
+public:
+ static ObjScopeGuardImpl0<Obj, MemFun> MakeObjGuard(Obj& obj, MemFun memFun)
+ {
+ return ObjScopeGuardImpl0<Obj, MemFun>(obj, memFun);
+ }
+ ~ObjScopeGuardImpl0() throw() { SafeExecute(*this); }
+ void Execute() { (obj_.*memFun_)(); }
+
+protected:
+ ObjScopeGuardImpl0(Obj& obj, MemFun memFun) : obj_(obj), memFun_(memFun) {}
+ Obj& obj_;
+ MemFun memFun_;
+};
+
+template <class Obj, typename MemFun>
+inline ObjScopeGuardImpl0<Obj, MemFun> MakeObjGuard(Obj& obj, MemFun memFun)
+{
+ return ObjScopeGuardImpl0<Obj, MemFun>::MakeObjGuard(obj, memFun);
+}
+
+template <typename Ret, class Obj1, class Obj2>
+inline ObjScopeGuardImpl0<Obj1, Ret (Obj2::*)()> MakeGuard(Ret (Obj2::*memFun)(),
+ Obj1& obj)
+{
+ return ObjScopeGuardImpl0<Obj1, Ret (Obj2::*)()>::MakeObjGuard(obj, memFun);
+}
+
+template <typename Ret, class Obj1, class Obj2>
+inline ObjScopeGuardImpl0<Obj1, Ret (Obj2::*)()> MakeGuard(Ret (Obj2::*memFun)(),
+ Obj1* obj)
+{
+ return ObjScopeGuardImpl0<Obj1, Ret (Obj2::*)()>::MakeObjGuard(*obj, memFun);
+}
+
+template <class Obj, typename MemFun, typename P1>
+class ObjScopeGuardImpl1 : public ScopeGuardImplBase
+{
+public:
+ static ObjScopeGuardImpl1<Obj, MemFun, P1> MakeObjGuard(Obj& obj, MemFun memFun,
+ P1 p1)
+ {
+ return ObjScopeGuardImpl1<Obj, MemFun, P1>(obj, memFun, p1);
+ }
+ ~ObjScopeGuardImpl1() throw() { SafeExecute(*this); }
+ void Execute() { (obj_.*memFun_)(p1_); }
+
+protected:
+ ObjScopeGuardImpl1(Obj& obj, MemFun memFun, P1 p1)
+ : obj_(obj), memFun_(memFun), p1_(p1)
+ {}
+ Obj& obj_;
+ MemFun memFun_;
+ const P1 p1_;
+};
+
+template <class Obj, typename MemFun, typename P1>
+inline ObjScopeGuardImpl1<Obj, MemFun, P1> MakeObjGuard(Obj& obj, MemFun memFun, P1 p1)
+{
+ return ObjScopeGuardImpl1<Obj, MemFun, P1>::MakeObjGuard(obj, memFun, p1);
+}
+
+template <typename Ret, class Obj1, class Obj2, typename P1a, typename P1b>
+inline ObjScopeGuardImpl1<Obj1, Ret (Obj2::*)(P1a), P1b>
+MakeGuard(Ret (Obj2::*memFun)(P1a), Obj1& obj, P1b p1)
+{
+ return ObjScopeGuardImpl1<Obj1, Ret (Obj2::*)(P1a), P1b>::MakeObjGuard(obj, memFun,
+ p1);
+}
+
+template <typename Ret, class Obj1, class Obj2, typename P1a, typename P1b>
+inline ObjScopeGuardImpl1<Obj1, Ret (Obj2::*)(P1a), P1b>
+MakeGuard(Ret (Obj2::*memFun)(P1a), Obj1* obj, P1b p1)
+{
+ return ObjScopeGuardImpl1<Obj1, Ret (Obj2::*)(P1a), P1b>::MakeObjGuard(*obj, memFun,
+ p1);
+}
+
+template <class Obj, typename MemFun, typename P1, typename P2>
+class ObjScopeGuardImpl2 : public ScopeGuardImplBase
+{
+public:
+ static ObjScopeGuardImpl2<Obj, MemFun, P1, P2> MakeObjGuard(Obj& obj, MemFun memFun,
+ P1 p1, P2 p2)
+ {
+ return ObjScopeGuardImpl2<Obj, MemFun, P1, P2>(obj, memFun, p1, p2);
+ }
+ ~ObjScopeGuardImpl2() throw() { SafeExecute(*this); }
+ void Execute() { (obj_.*memFun_)(p1_, p2_); }
+
+protected:
+ ObjScopeGuardImpl2(Obj& obj, MemFun memFun, P1 p1, P2 p2)
+ : obj_(obj), memFun_(memFun), p1_(p1), p2_(p2)
+ {}
+ Obj& obj_;
+ MemFun memFun_;
+ const P1 p1_;
+ const P2 p2_;
+};
+
+template <class Obj, typename MemFun, typename P1, typename P2>
+inline ObjScopeGuardImpl2<Obj, MemFun, P1, P2> MakeObjGuard(Obj& obj, MemFun memFun,
+ P1 p1, P2 p2)
+{
+ return ObjScopeGuardImpl2<Obj, MemFun, P1, P2>::MakeObjGuard(obj, memFun, p1, p2);
+}
+
+template <typename Ret, class Obj1, class Obj2, typename P1a, typename P1b,
+ typename P2a, typename P2b>
+inline ObjScopeGuardImpl2<Obj1, Ret (Obj2::*)(P1a, P2a), P1b, P2b>
+MakeGuard(Ret (Obj2::*memFun)(P1a, P2a), Obj1& obj, P1b p1, P2b p2)
+{
+ return ObjScopeGuardImpl2<Obj1, Ret (Obj2::*)(P1a, P2a), P1b, P2b>::MakeObjGuard(
+ obj, memFun, p1, p2);
+}
+
+template <typename Ret, class Obj1, class Obj2, typename P1a, typename P1b,
+ typename P2a, typename P2b>
+inline ObjScopeGuardImpl2<Obj1, Ret (Obj2::*)(P1a, P2a), P1b, P2b>
+MakeGuard(Ret (Obj2::*memFun)(P1a, P2a), Obj1* obj, P1b p1, P2b p2)
+{
+ return ObjScopeGuardImpl2<Obj1, Ret (Obj2::*)(P1a, P2a), P1b, P2b>::MakeObjGuard(
+ *obj, memFun, p1, p2);
+}
+
+#define CONCATENATE_DIRECT(s1, s2) s1##s2
+#define CONCATENATE(s1, s2) CONCATENATE_DIRECT(s1, s2)
+#define ANONYMOUS_VARIABLE(str) CONCATENATE(str, __LINE__)
+
+// #define ON_BLOCK_EXIT ScopeGuard ANONYMOUS_VARIABLE(scopeGuard) = MakeGuard
+#define ON_BLOCK_EXIT(arg) \
+ ScopeGuard ANONYMOUS_VARIABLE(scopeGuard) = MakeGuard(arg); \
+ UNUSED_VAR(ANONYMOUS_VARIABLE(scopeGuard))
+// #define ON_BLOCK_EXIT_OBJ ScopeGuard ANONYMOUS_VARIABLE(scopeGuard) = MakeObjGuard
+
+#define UNUSED_VAR(VAR) (void)VAR
+
+namespace MOBase
+{
+
+} // namespace MOBase
+
+#endif // SCOPEGUARD_H
diff --git a/libs/uibase/include/uibase/sortabletreewidget.h b/libs/uibase/include/uibase/sortabletreewidget.h
new file mode 100644
index 0000000..4dc7881
--- /dev/null
+++ b/libs/uibase/include/uibase/sortabletreewidget.h
@@ -0,0 +1,64 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2013 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 SORTABLETREEWIDGET_H
+#define SORTABLETREEWIDGET_H
+
+#include "dllimport.h"
+#include <QTreeWidget>
+
+namespace MOBase
+{
+
+class QDLLEXPORT SortableTreeWidget : public QTreeWidget
+{
+ Q_OBJECT
+
+public:
+ SortableTreeWidget(QWidget* parent = nullptr);
+
+ /**
+ * @brief sets whether sorting is allowed only within the branch of a tree
+ * @param localOnly if true, objects can only be moved within a branch. if false
+ * (default) they can be moved to a different branch
+ */
+ void setLocalMoveOnly(bool localOnly);
+signals:
+ /**
+ * @brief signaled when items got moved. minimal atm
+ */
+ void itemsMoved();
+
+protected:
+ virtual void dropEvent(QDropEvent* event);
+ virtual bool dropMimeData(QTreeWidgetItem* parent, int index, const QMimeData* data,
+ Qt::DropAction action);
+ virtual Qt::DropActions supportedDropActions() const;
+
+private:
+ bool moveSelection(QTreeWidgetItem* parent, int index);
+
+private:
+ bool m_LocalMoveOnly;
+};
+
+} // namespace MOBase
+
+#endif // SORTABLETREEWIDGET_H
diff --git a/libs/uibase/include/uibase/steamutility.h b/libs/uibase/include/uibase/steamutility.h
new file mode 100644
index 0000000..4789ad3
--- /dev/null
+++ b/libs/uibase/include/uibase/steamutility.h
@@ -0,0 +1,47 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2019 MO2 Contributors. 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 STEAMUTILITY_H
+#define STEAMUTILITY_H
+
+#include "dllimport.h"
+#include <QList>
+#include <QString>
+
+namespace MOBase
+{
+
+/**
+ * @brief Gets the installation path to Steam according to the registy
+ **/
+QDLLEXPORT QString findSteam();
+
+/**
+ * @brief Gets the installation path to a Steam game
+ *
+ * @param appName The Steam application name, i.e., the expected steamapps folder name
+ * @param validFile If the given file exists in the found installation path, the game is
+ consider to be valid. May be blank.
+ **/
+QDLLEXPORT QString findSteamGame(const QString& appName, const QString& validFile);
+
+} // namespace MOBase
+
+#endif // STEAMUTILITY_H
diff --git a/libs/uibase/include/uibase/strings.h b/libs/uibase/include/uibase/strings.h
new file mode 100644
index 0000000..be388ab
--- /dev/null
+++ b/libs/uibase/include/uibase/strings.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <string>
+#include <string_view>
+
+#include "dllimport.h"
+
+namespace MOBase
+{
+#ifdef __cplusplus
+extern "C++" {
+#endif
+
+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);
+
+#ifdef __cplusplus
+}
+#endif
+} // namespace MOBase
diff --git a/libs/uibase/include/uibase/taskprogressmanager.h b/libs/uibase/include/uibase/taskprogressmanager.h
new file mode 100644
index 0000000..8dba8d1
--- /dev/null
+++ b/libs/uibase/include/uibase/taskprogressmanager.h
@@ -0,0 +1,60 @@
+#ifndef TASKPROGRESSMANAGER_H
+#define TASKPROGRESSMANAGER_H
+
+#include "dllimport.h"
+#include <QMutexLocker>
+#include <QObject>
+#include <QTime>
+#include <QTimer>
+#include <cstdint>
+#include <map>
+
+#ifdef _WIN32
+#include <Windows.h>
+#include <shobjidl.h>
+#else
+using HWND = void*;
+#endif
+
+namespace MOBase
+{
+
+class QDLLEXPORT TaskProgressManager : QObject
+{
+
+ Q_OBJECT
+
+public:
+ static TaskProgressManager& instance();
+
+ void forgetMe(quint32 id);
+ void updateProgress(quint32 id, qint64 value, qint64 max);
+
+ quint32 getId();
+
+public slots:
+ bool tryCreateTaskbar();
+
+private:
+ TaskProgressManager();
+ void showProgress();
+
+private:
+ std::map<quint32, std::pair<QTime, qint64>> m_Percentages;
+ QMutex m_Mutex;
+ quint32 m_NextId;
+ QTimer m_CreateTimer;
+ int m_CreateTries;
+
+ HWND m_WinId;
+
+#ifdef _WIN32
+ ITaskbarList3* m_Taskbar;
+#else
+ void* m_Taskbar;
+#endif
+};
+
+} // namespace MOBase
+
+#endif // TASKPROGRESSMANAGER_H
diff --git a/libs/uibase/include/uibase/textviewer.h b/libs/uibase/include/uibase/textviewer.h
new file mode 100644
index 0000000..4f5dfa2
--- /dev/null
+++ b/libs/uibase/include/uibase/textviewer.h
@@ -0,0 +1,97 @@
+/*
+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 TEXTVIEWER_H
+#define TEXTVIEWER_H
+
+#include "dllimport.h"
+#include <QDialog>
+#include <QTabWidget>
+#include <QTextEdit>
+#include <set>
+
+namespace Ui
+{
+class TextViewer;
+}
+
+namespace MOBase
+{
+
+class FindDialog;
+
+/**
+ * @brief rudimentary tabbed text editor
+ **/
+class QDLLEXPORT TextViewer : public QDialog
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ * @param parent parent widget
+ **/
+ explicit TextViewer(const QString& title, QWidget* parent = 0);
+
+ ~TextViewer();
+
+ /**
+ * @brief set the description text to inform the user what he's editing
+ *
+ * @param description the description to set
+ **/
+ void setDescription(const QString& description);
+
+ /**
+ * @brief add a new tab with the specified file open
+ *
+ * @param fileName name of the file to open
+ * @param writable if true, the file can be modified
+ **/
+ void addFile(const QString& fileName, bool writable);
+
+protected:
+ void closeEvent(QCloseEvent* event);
+ bool eventFilter(QObject* obj, QEvent* event);
+
+private slots:
+
+ void saveFile();
+ void modified();
+ void patternChanged(QString newPattern);
+ void findNext();
+ void showWhitespaceChanged(int state);
+
+private:
+ void saveFile(const QTextEdit* editor);
+ void find();
+
+private:
+ Ui::TextViewer* ui;
+ QTabWidget* m_EditorTabs;
+ std::set<QTextEdit*> m_Modified;
+ FindDialog* m_FindDialog;
+ QString m_FindPattern;
+};
+
+} // namespace MOBase
+
+#endif // TEXTVIEWER_H
diff --git a/libs/uibase/include/uibase/tutorabledialog.h b/libs/uibase/include/uibase/tutorabledialog.h
new file mode 100644
index 0000000..aaaa093
--- /dev/null
+++ b/libs/uibase/include/uibase/tutorabledialog.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 TUTORABLEDIALOG_H
+#define TUTORABLEDIALOG_H
+
+#include "dllimport.h"
+#include "tutorialcontrol.h"
+#include <QDialog>
+#include <QResizeEvent>
+#include <QShowEvent>
+
+namespace MOBase
+{
+
+/**
+ * @brief A dialog for which a tutorial can be displayed. Dialogs should derive from
+ * this instead of QDialog and delegate all showEvent- and resizeEvent-calls to
+ * TutorableDialog for tutorials to work
+ */
+class QDLLEXPORT TutorableDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ /**
+ * @brief constructor
+ * @param name a unique name for this dialog type. This is used to refer to the dialog
+ * from tutorials
+ * @param parent the parent widget of the dialog
+ */
+ explicit TutorableDialog(const QString& name, QWidget* parent = 0);
+
+signals:
+
+public slots:
+
+protected:
+ void showEvent(QShowEvent* event);
+ void resizeEvent(QResizeEvent* event);
+
+private:
+ TutorialControl m_Tutorial;
+};
+
+} // namespace MOBase
+
+#endif // TUTORABLEDIALOG_H
diff --git a/libs/uibase/include/uibase/tutorialcontrol.h b/libs/uibase/include/uibase/tutorialcontrol.h
new file mode 100644
index 0000000..4dc85f6
--- /dev/null
+++ b/libs/uibase/include/uibase/tutorialcontrol.h
@@ -0,0 +1,83 @@
+/*
+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 TUTORIALCONTROL_H
+#define TUTORIALCONTROL_H
+
+#include "dllimport.h"
+#include <QQuickWidget>
+#include <QRect>
+#include <QWidget>
+#include <utility>
+
+namespace MOBase
+{
+
+class TutorialManager;
+
+class QDLLEXPORT TutorialControl : public QObject
+{
+ Q_OBJECT
+public:
+ TutorialControl(const TutorialControl& reference);
+ TutorialControl(QWidget* targetControl, const QString& name);
+
+ ~TutorialControl();
+
+ void registerControl();
+ void resize(const QSize& size);
+
+ void expose(const QString& widgetName, QObject* widget);
+
+ void startTutorial(const QString& tutorial);
+
+ Q_INVOKABLE void finish();
+ Q_INVOKABLE QRect getRect(const QString& widgetName);
+ Q_INVOKABLE QRect getActionRect(const QString& widgetName);
+ Q_INVOKABLE QRect getMenuRect(const QString& widgetName);
+ Q_INVOKABLE QWidget* getChild(const QString& name);
+ Q_INVOKABLE bool waitForButton(const QString& buttonName);
+ Q_INVOKABLE bool waitForAction(const QString& actionName);
+ Q_INVOKABLE bool waitForTabOpen(const QString& tabControlName, const QString& tab);
+ Q_INVOKABLE const QString getTabName(const QString& tabControlName);
+ Q_INVOKABLE void lockUI(bool locked);
+ Q_INVOKABLE void simulateClick(int x, int y);
+
+private slots:
+
+ void tabChangedProxy(int selected);
+ void nextTutorialStepProxy();
+
+private:
+ QWidget* m_TargetControl;
+ QString m_Name;
+ QQuickWidget* m_TutorialView;
+
+ TutorialManager& m_Manager;
+
+ std::vector<std::pair<QString, QObject*>> m_ExposedObjects;
+ QList<QMetaObject::Connection> m_Connections;
+ int m_ExpectedTab;
+ QWidget* m_CurrentClickControl;
+};
+
+} // namespace MOBase
+
+#endif // TUTORIALCONTROL_H
diff --git a/libs/uibase/include/uibase/tutorialmanager.h b/libs/uibase/include/uibase/tutorialmanager.h
new file mode 100644
index 0000000..8120164
--- /dev/null
+++ b/libs/uibase/include/uibase/tutorialmanager.h
@@ -0,0 +1,109 @@
+/*
+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 TUTORIALMANAGER_H
+#define TUTORIALMANAGER_H
+
+#include "dllimport.h"
+#include <QObject>
+#include <map>
+
+namespace MOBase
+{
+
+class TutorialControl;
+
+class QDLLEXPORT TutorialManager : public QObject
+{
+
+ Q_OBJECT
+
+public:
+ /**
+ * @brief set up the tutorial manager
+ * @param path to where the tutorials are stored
+ */
+ static void init(const QString& tutorialPath, QObject* organizerCore);
+
+ static TutorialManager& instance();
+
+ QObject* organizerCore() { return m_OrganizerCore; }
+
+ /**
+ * @brief registers a control that can be used to display tutorial messages in one
+ * window. This this called when the window becomes visible
+ * @param windowName name of the window. This is used to reference the window from
+ * qml/js files, as well as for the unregister-call
+ * @param control the control to register
+ */
+ void registerControl(const QString& windowName, TutorialControl* control);
+
+ /**
+ * @brief unregister a tutorial control. This is called when the window to which the
+ * control belongs gets closed
+ * @param windowName name of the control to hide
+ */
+ void unregisterControl(const QString& windowName);
+
+ /**
+ * @brief tests if the specified tutorial exists
+ * @param tutorialName name of the tutorial to test
+ * @return true if there is a tutorial with the specified name
+ */
+ bool hasTutorial(const QString& tutorialName);
+
+ /**
+ * @brief start a tutorial for the specified window
+ * @param windowName window for which to start the tutorial
+ * @param tutorialName name of the tutorial script to run
+ */
+ Q_INVOKABLE void activateTutorial(const QString& windowName,
+ const QString& tutorialName);
+
+ /**
+ * @brief mark a window tutorial as completed. It will not show up again
+ * @param windowName name of the window for which the tutorial completed
+ */
+ Q_INVOKABLE void finishWindowTutorial(const QString& windowName);
+
+ Q_INVOKABLE QWidget* findControl(const QString& controlName);
+signals:
+
+ void windowTutorialFinished(const QString& windowName);
+
+ void tabChanged(int index);
+
+private:
+ TutorialManager(const QString& tutorialPath, QObject* organizerCore);
+
+private:
+ static TutorialManager* s_Instance;
+
+ // QScriptEngine m_ScriptEngine;
+ QString m_TutorialPath;
+ QObject* m_OrganizerCore;
+
+ std::map<QString, TutorialControl*> m_Controls;
+ std::map<QString, QString> m_PendingTutorials;
+};
+
+} // namespace MOBase
+
+#endif // TUTORIALMANAGER_H
diff --git a/libs/uibase/include/uibase/utility.h b/libs/uibase/include/uibase/utility.h
new file mode 100644
index 0000000..c08133b
--- /dev/null
+++ b/libs/uibase/include/uibase/utility.h
@@ -0,0 +1,443 @@
+/*
+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 MO_UIBASE_UTILITY_INCLUDED
+#define MO_UIBASE_UTILITY_INCLUDED
+
+#include <QDir>
+#include <QIcon>
+#include <QList>
+#include <QString>
+#include <QTextStream>
+#include <QUrl>
+#include <QVariant>
+#include <algorithm>
+#include <set>
+#include <vector>
+#include <cstdint>
+#include <fstream>
+
+#ifdef _WIN32
+#include <ShlObj.h>
+#include <Windows.h>
+#else
+// POSIX compatibility types
+#include "windows_compat.h"
+#endif
+
+#include "dllimport.h"
+#include "exceptions.h"
+
+namespace MOBase
+{
+
+/**
+ * @brief remove the specified directory including all sub-directories
+ *
+ * @param dirName name of the directory to delete
+ * @return true on success. in case of an error, "removeDir" itself displays an error
+ *message
+ **/
+QDLLEXPORT bool removeDir(const QString& dirName);
+
+/**
+ * @brief copy a directory recursively
+ * @param sourceName name of the directory to copy
+ * @param destinationName name of the target directory
+ * @param merge if true, the destination directory is allowed to exist, files will then
+ * be added to that directory. If false, the call will fail in that case
+ * @return true if files were copied. This doesn't necessary mean ALL files were copied
+ * @note symbolic links are not followed to prevent endless recursion
+ */
+QDLLEXPORT bool copyDir(const QString& sourceName, const QString& destinationName,
+ bool merge);
+
+/**
+ * @brief move a file, creating subdirectories as needed
+ * @param source source file name
+ * @param destination destination file name
+ * @return true if the file was successfully copied
+ */
+QDLLEXPORT bool moveFileRecursive(const QString& source, const QString& baseDir,
+ const QString& destination);
+
+/**
+ * @brief copy a file, creating subdirectories as needed
+ * @param source source file name
+ * @param destination destination file name
+ * @return true if the file was successfully copied
+ */
+QDLLEXPORT bool copyFileRecursive(const QString& source, const QString& baseDir,
+ const QString& destination);
+
+/**
+ * @brief copy one or multiple files using a shell operation (this will ask the user for
+ *confirmation on overwrite or elevation requirement)
+ * @param sourceNames names of files to be copied. This can include wildcards
+ * @param destinationNames names of the files in the destination location or the
+ *destination directory to copy to. There has to be one destination name for each source
+ *name or a single directory
+ * @param dialog a dialog to be the parent of possible confirmation dialogs
+ * @return true on success, false on error
+ **/
+QDLLEXPORT bool shellCopy(const QStringList& sourceNames,
+ const QStringList& destinationNames,
+ QWidget* dialog = nullptr);
+
+QDLLEXPORT bool shellCopy(const QString& sourceNames, const QString& destinationNames,
+ bool yesToAll = false, QWidget* dialog = nullptr);
+
+QDLLEXPORT bool shellMove(const QStringList& sourceNames,
+ const QStringList& destinationNames,
+ QWidget* dialog = nullptr);
+
+QDLLEXPORT bool shellMove(const QString& sourceNames, const QString& destinationNames,
+ bool yesToAll = false, QWidget* dialog = nullptr);
+
+QDLLEXPORT bool shellRename(const QString& oldName, const QString& newName,
+ bool yesToAll = false, QWidget* dialog = nullptr);
+
+QDLLEXPORT bool shellDelete(const QStringList& fileNames, bool recycle = false,
+ QWidget* dialog = nullptr);
+
+QDLLEXPORT bool shellDeleteQuiet(const QString& fileName, QWidget* dialog = nullptr);
+
+namespace shell
+{
+ namespace details
+ {
+ // used by HandlePtr on Windows, stub on Linux
+ struct HandleCloser
+ {
+ using pointer = HANDLE;
+
+ void operator()(HANDLE h)
+ {
+#ifdef _WIN32
+ if (h != INVALID_HANDLE_VALUE) {
+ ::CloseHandle(h);
+ }
+#else
+ (void)h;
+#endif
+ }
+ };
+
+ using HandlePtr = std::unique_ptr<HANDLE, HandleCloser>;
+ } // namespace details
+
+ // returned by the various shell functions
+ class QDLLEXPORT Result
+ {
+ public:
+ Result(bool success, DWORD error, QString message, HANDLE process);
+
+ // non-copyable
+ Result(const Result&) = delete;
+ Result& operator=(const Result&) = delete;
+ Result(Result&&) = default;
+ Result& operator=(Result&&) = default;
+
+ static Result makeFailure(DWORD error, QString message = {});
+ static Result makeSuccess(HANDLE process = INVALID_HANDLE_VALUE);
+
+ bool success() const;
+ explicit operator bool() const;
+
+ DWORD error();
+
+ const QString& message() const;
+
+ HANDLE processHandle() const;
+
+ HANDLE stealProcessHandle();
+
+ QString toString() const;
+
+ private:
+ bool m_success;
+ DWORD m_error;
+ QString m_message;
+ details::HandlePtr m_process;
+ };
+
+ QDLLEXPORT QString formatError(int i);
+
+ QDLLEXPORT Result Explore(const QFileInfo& info);
+ QDLLEXPORT Result Explore(const QString& path);
+ QDLLEXPORT Result Explore(const QDir& dir);
+
+ QDLLEXPORT Result Open(const QString& path);
+ QDLLEXPORT Result Open(const QUrl& url);
+
+ QDLLEXPORT Result Execute(const QString& program, const QString& params = {});
+
+ QDLLEXPORT Result Delete(const QFileInfo& path);
+
+ QDLLEXPORT Result Rename(const QFileInfo& src, const QFileInfo& dest);
+ QDLLEXPORT Result Rename(const QFileInfo& src, const QFileInfo& dest,
+ bool copyAllowed);
+
+ QDLLEXPORT Result CreateDirectories(const QDir& dir);
+ QDLLEXPORT Result DeleteDirectoryRecursive(const QDir& dir);
+
+ QDLLEXPORT void SetUrlHandler(const QString& cmd);
+} // namespace shell
+
+template <typename T>
+QString VectorJoin(const std::vector<T>& value, const QString& separator,
+ size_t maximum = UINT_MAX)
+{
+ QString result;
+ if (value.size() != 0) {
+ QTextStream stream(&result);
+ stream << value[0];
+ for (unsigned int i = 1; i < (std::min)(value.size(), maximum); ++i) {
+ stream << separator << value[i];
+ }
+ if (maximum < value.size()) {
+ stream << separator << "...";
+ }
+ }
+ return result;
+}
+
+template <typename T>
+QString SetJoin(const std::set<T>& value, const QString& separator,
+ size_t maximum = UINT_MAX)
+{
+ QString result;
+ typename std::set<T>::const_iterator iter = value.begin();
+ if (iter != value.end()) {
+ QTextStream stream(&result);
+ stream << *iter;
+ ++iter;
+ unsigned int pos = 1;
+ for (; iter != value.end() && pos < maximum; ++iter) {
+ stream << separator << *iter;
+ }
+ if (maximum < value.size()) {
+ stream << separator << "...";
+ }
+ }
+ return result;
+}
+
+template <typename T>
+QList<T> ConvertList(const QVariantList& variants)
+{
+ QList<T> result;
+ for (const QVariant& var : variants) {
+ if (!var.canConvert<T>()) {
+ throw Exception("invalid variant type");
+ }
+ result.append(var.value<T>());
+ }
+}
+
+QDLLEXPORT std::wstring ToWString(const QString& source);
+QDLLEXPORT std::string ToString(const QString& source, bool utf8 = true);
+QDLLEXPORT QString ToQString(const std::string& source);
+QDLLEXPORT QString ToQString(const std::wstring& source);
+
+#ifdef _WIN32
+QDLLEXPORT QString ToString(const SYSTEMTIME& time);
+#endif
+
+QDLLEXPORT int naturalCompare(const QString& a, const QString& b,
+ Qt::CaseSensitivity cs = Qt::CaseInsensitive);
+
+class QDLLEXPORT NaturalSort
+{
+public:
+ NaturalSort(Qt::CaseSensitivity cs = Qt::CaseInsensitive) : m_cs(cs) {}
+
+ bool operator()(const QString& a, const QString& b)
+ {
+ return (naturalCompare(a, b, m_cs) < 0);
+ }
+
+private:
+ Qt::CaseSensitivity m_cs;
+};
+
+#ifdef _WIN32
+QDLLEXPORT QDir getKnownFolder(KNOWNFOLDERID id, const QString& what = {});
+QDLLEXPORT QString getOptionalKnownFolder(KNOWNFOLDERID id);
+#endif
+
+QDLLEXPORT QString getDesktopDirectory();
+QDLLEXPORT QString getStartMenuDirectory();
+
+QDLLEXPORT QString readFileText(const QString& fileName, QString* encoding = nullptr,
+ bool* hadBOM = nullptr);
+
+QDLLEXPORT QString decodeTextData(const QByteArray& fileData,
+ QString* encoding = nullptr, bool* hadBOM = nullptr);
+
+QDLLEXPORT void removeOldFiles(const QString& path, const QString& pattern,
+ int numToKeep, QDir::SortFlags sorting = QDir::Time);
+
+QDLLEXPORT QIcon iconForExecutable(const QString& filePath);
+
+QDLLEXPORT QString getFileVersion(QString const& filepath);
+QDLLEXPORT QString getProductVersion(QString const& program);
+
+QDLLEXPORT bool isWindowsDrivePath(const QString& path);
+QDLLEXPORT bool isWineZDrivePath(const QString& path);
+QDLLEXPORT QString toWinePath(const QString& path);
+QDLLEXPORT QString fromWinePath(const QString& path);
+QDLLEXPORT QString normalizePathForHost(const QString& path);
+QDLLEXPORT QString normalizePathForWine(const QString& path);
+
+QDLLEXPORT void deleteChildWidgets(QWidget* w);
+
+template <typename T>
+bool isOneOf(const T& val, const std::initializer_list<T>& list)
+{
+ return std::find(list.begin(), list.end(), val) != list.end();
+}
+
+QDLLEXPORT std::wstring formatSystemMessage(DWORD id);
+QDLLEXPORT std::wstring formatNtMessage(NTSTATUS s);
+
+inline std::wstring formatSystemMessage(HRESULT hr)
+{
+ return formatSystemMessage(static_cast<DWORD>(hr));
+}
+
+QDLLEXPORT QString windowsErrorString(DWORD errorCode);
+
+QDLLEXPORT QString localizedByteSize(unsigned long long bytes);
+QDLLEXPORT QString localizedByteSpeed(unsigned long long bytesPerSecond);
+
+QDLLEXPORT QString localizedTimeRemaining(unsigned int msecs);
+
+template <class F>
+class Guard
+{
+public:
+ Guard() : m_call(false) {}
+
+ Guard(F f) : m_f(f), m_call(true) {}
+
+ Guard(Guard&& g) : m_f(std::move(g.m_f)) { g.m_call = false; }
+
+ ~Guard()
+ {
+ if (m_call)
+ m_f();
+ }
+
+ Guard& operator=(Guard&& g)
+ {
+ m_f = std::move(g.m_f);
+ g.m_call = false;
+ return *this;
+ }
+
+ void kill() { m_call = false; }
+
+ Guard(const Guard&) = delete;
+ Guard& operator=(const Guard&) = delete;
+
+private:
+ F m_f;
+ bool m_call;
+};
+
+class QDLLEXPORT TimeThis
+{
+public:
+ TimeThis(const QString& what = {});
+ ~TimeThis();
+
+ TimeThis(const TimeThis&) = delete;
+ TimeThis& operator=(const TimeThis&) = delete;
+
+ void start(const QString& what = {});
+ void stop();
+
+private:
+ using Clock = std::chrono::high_resolution_clock;
+
+ QString m_what;
+ Clock::time_point m_start;
+ bool m_running;
+};
+
+template <class F>
+bool forEachLineInFile(const QString& filePath, F&& f)
+{
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ return false;
+ }
+
+ QByteArray data = file.readAll();
+ file.close();
+
+ const char* lineStart = data.constData();
+ const char* p = lineStart;
+ const char* end = data.constData() + data.size();
+
+ while (p < end) {
+ // skip all newline characters
+ while ((p < end) && (*p == '\n' || *p == '\r')) {
+ ++p;
+ }
+
+ // line starts here
+ lineStart = p;
+
+ // find end of line
+ while ((p < end) && *p != '\n' && *p != '\r') {
+ ++p;
+ }
+
+ if (p != lineStart) {
+ // skip whitespace at beginning of line, don't go past end of line
+ while (std::isspace(*lineStart) && lineStart < p) {
+ ++lineStart;
+ }
+
+ // skip comments
+ if (*lineStart != '#') {
+ // skip line if it only had whitespace
+ if (lineStart < p) {
+ // skip white at end of line
+ const char* lineEnd = p - 1;
+ while (std::isspace(*lineEnd) && lineEnd > lineStart) {
+ --lineEnd;
+ }
+ ++lineEnd;
+
+ f(QString::fromUtf8(lineStart, lineEnd - lineStart));
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+} // namespace MOBase
+
+#endif // MO_UIBASE_UTILITY_INCLUDED
diff --git a/libs/uibase/include/uibase/versioninfo.h b/libs/uibase/include/uibase/versioninfo.h
new file mode 100644
index 0000000..fd5d481
--- /dev/null
+++ b/libs/uibase/include/uibase/versioninfo.h
@@ -0,0 +1,185 @@
+/*
+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 MODVERSION_H
+#define MODVERSION_H
+
+#include "dllimport.h"
+#include <QList>
+#include <QString>
+
+class QVersionNumber;
+
+namespace MOBase
+{
+
+/**
+ * @brief represents the version of a mod or plugin
+ *
+ * this will try to parse machine-readable information from a string.
+ **/
+class QDLLEXPORT VersionInfo
+{
+
+ friend QDLLEXPORT bool operator<(const VersionInfo& LHS, const VersionInfo& RHS);
+ friend QDLLEXPORT bool operator>(const VersionInfo& LHS, const VersionInfo& RHS);
+ friend QDLLEXPORT bool operator<=(const VersionInfo& LHS, const VersionInfo& RHS);
+ friend QDLLEXPORT bool operator>=(const VersionInfo& LHS, const VersionInfo& RHS);
+ friend QDLLEXPORT bool operator!=(const VersionInfo& LHS, const VersionInfo& RHS);
+ friend QDLLEXPORT bool operator==(const VersionInfo& LHS, const VersionInfo& RHS);
+
+public:
+ enum ReleaseType
+ {
+ RELEASE_PREALPHA,
+ RELEASE_ALPHA,
+ RELEASE_BETA,
+ RELEASE_CANDIDATE,
+ RELEASE_FINAL
+ };
+
+ enum VersionScheme
+ {
+ SCHEME_DISCOVER, // use regular scheme unless the string contains a hint that it's
+ // one of the others
+ SCHEME_REGULAR,
+ SCHEME_DECIMALMARK, // for schemes that treat the version as a decimal number with
+ // the dot as the decimal mark
+ SCHEME_NUMBERSANDLETTERS, // for schemes that mix numbers and letters
+ // (1.0.1a, 1.0.1c, ...). otherwise this is the regular
+ // scheme
+ SCHEME_DATE, // contains a release date instead of a version number
+ SCHEME_LITERAL // use the version string as is, unmodified
+ };
+
+public:
+ /**
+ * @brief default constructor
+ * constructs an invalid version
+ **/
+ VersionInfo();
+
+ /**
+ * @brief constructor
+ * @param major major version
+ * @param minor minor version
+ * @param subminor subminor version
+ * @param releaseType release type
+ */
+ VersionInfo(int major, int minor, int subminor,
+ ReleaseType releaseType = RELEASE_FINAL);
+
+ /**
+ * @brief constructor
+ * @param major major version
+ * @param minor minor version
+ * @param subminor subminor version
+ * @param subsubminor subsubminor version
+ * @param releaseType release type
+ */
+ VersionInfo(int major, int minor, int subminor, int subsubminor,
+ ReleaseType releaseType = RELEASE_FINAL);
+
+ /**
+ * @brief constructor
+ * @param versionString the string to construct from
+ **/
+ VersionInfo(const QString& versionString, VersionScheme scheme = SCHEME_DISCOVER);
+
+ /**
+ * @brief constructor
+ * @param versionString the string to construct from
+ * @param manualInput if true the versionString is treated as input from a user
+ **/
+ VersionInfo(const QString& versionString, VersionScheme scheme, bool manualInput);
+
+ /**
+ * @brief resets this structure to an invalid version
+ */
+ void clear();
+
+ /**
+ * @brief parse the version from the specified string
+ *
+ * @param versionString the string to parse
+ **/
+ void parse(const QString& versionString, VersionScheme scheme = SCHEME_DISCOVER,
+ bool manualInput = false);
+
+ /**
+ * @return a canonicalized version string
+ * @note due to support for different versioning schemes this somewhat lost it's
+ *original intention. This is now supposed to return a version string that can be
+ *parsed to re-create this VersionInfo without information loss.
+ **/
+ QString canonicalString() const;
+
+ /**
+ * @return a version string for display to the user. This may loose information as it
+ * doesn't contain information about the versioning scheme
+ *
+ * @param forcedVersionSegments the number of version segments to display even if the
+ * version is 0. 1 is major, 2 is major and minor, etc. The only implemented ranges
+ * are (-inf,2] for major/minor, [3] for major/minor/subminor, and [4,inf) for
+ * major/minor/subminor/subsubminor. This only versions with a regular scheme.
+ */
+ QString displayString(int forcedVersionSegments = 2) const;
+
+ /**
+ * @return true if this version is valid, false if it wasn't initialised or
+ * the version string was not parsable
+ */
+ bool isValid() const { return m_Valid; }
+
+ /**
+ * @return the versioning scheme in effect
+ */
+ VersionScheme scheme() const { return m_Scheme; }
+
+ // returns this version number as a QVersionNumber
+ //
+ QVersionNumber asQVersionNumber() const;
+
+private:
+ /**
+ * @brief determine the release type
+ * @param versionString the version string to parse
+ * @return the version string with the release type removed
+ **/
+ QString parseReleaseType(QString versionString);
+
+private:
+ VersionScheme m_Scheme;
+
+ bool m_Valid;
+ ReleaseType m_ReleaseType;
+ int m_Major;
+ int m_Minor;
+ int m_SubMinor;
+ int m_SubSubMinor;
+
+ int m_DecimalPositions;
+
+ QString m_Rest;
+};
+
+} // namespace MOBase
+
+#endif // MODVERSION_H
diff --git a/libs/uibase/include/uibase/versioning.h b/libs/uibase/include/uibase/versioning.h
new file mode 100644
index 0000000..c4f9541
--- /dev/null
+++ b/libs/uibase/include/uibase/versioning.h
@@ -0,0 +1,181 @@
+#pragma once
+
+#include <compare>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include <QFlags>
+#include <QString>
+
+#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<std::variant<int, ReleaseType>> 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<std::variant<int, ReleaseType>> 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 <class CharT>
+struct std::formatter<MOBase::Version, CharT> : std::formatter<QString, CharT>
+{
+ template <class FmtContext>
+ FmtContext::iterator format(const MOBase::Version& v, FmtContext& ctx) const
+ {
+ return std::formatter<QString, CharT>::format(v.string(), ctx);
+ }
+};
diff --git a/libs/uibase/include/uibase/widgetutility.h b/libs/uibase/include/uibase/widgetutility.h
new file mode 100644
index 0000000..10dc6f9
--- /dev/null
+++ b/libs/uibase/include/uibase/widgetutility.h
@@ -0,0 +1,22 @@
+#ifndef UIBASE_WIDGETUTILITY_INCLUDED
+#define UIBASE_WIDGETUTILITY_INCLUDED
+
+#include "dllimport.h"
+#include <QAbstractItemView>
+#include <QTreeView>
+
+namespace MOBase
+{
+
+/**
+ * Custom user-role that can be used in conjunction with `setCustomizableColumns`. If
+ * a column has a value for this role, and the value is false, the checkbox
+ * corresponding to the column will be disabled, otherwise the checkbox is enabled.
+ */
+constexpr auto EnabledColumnRole = Qt::UserRole + 1;
+
+QDLLEXPORT void setCustomizableColumns(QTreeView* view);
+
+} // namespace MOBase
+
+#endif // UIBASE_WIDGETUTILITY_INCLUDED
diff --git a/libs/uibase/include/uibase/windows_compat.h b/libs/uibase/include/uibase/windows_compat.h
new file mode 120000
index 0000000..bce219f
--- /dev/null
+++ b/libs/uibase/include/uibase/windows_compat.h
@@ -0,0 +1 @@
+../../../../src/src/shared/windows_compat.h \ No newline at end of file
diff --git a/libs/uibase/src/CMakeLists.txt b/libs/uibase/src/CMakeLists.txt
new file mode 100644
index 0000000..fff3ac3
--- /dev/null
+++ b/libs/uibase/src/CMakeLists.txt
@@ -0,0 +1,201 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Qt6 CONFIG REQUIRED COMPONENTS Network Qml Quick QuickWidgets Widgets)
+find_package(spdlog CONFIG REQUIRED)
+qt_standard_project_setup()
+
+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/iexecutable.h
+ ../include/uibase/iexecutableslist.h
+ ../include/uibase/ifiletree.h
+ ../include/uibase/ifiletree_utils.h
+ ../include/uibase/iinstallationmanager.h
+ ../include/uibase/iinstance.h
+ ../include/uibase/iinstancemanager.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/strings.h
+ ../include/uibase/formatters.h
+)
+
+# Collect all source files
+set(UIBASE_SOURCES
+ # root sources
+ delayedfilewriter.cpp
+ diagnosisreport.cpp
+ errorcodes.cpp
+ eventfilter.cpp
+ executableinfo.cpp
+ filesystemutilities.cpp
+ guessedvalue.cpp
+ json.cpp
+ log.cpp
+ modrepositoryfileinfo.cpp
+ nxmurl.cpp
+ pluginrequirements.cpp
+ registry.cpp
+ report.cpp
+ safewritefile.cpp
+ scopeguard.cpp
+ steamutility.cpp
+ strings.cpp
+ utility.cpp
+ versioning.cpp
+ versioninfo.cpp
+ # interfaces
+ ifiletree.cpp
+ imodrepositorybridge.cpp
+ imoinfo.cpp
+ # tutorials
+ tutorabledialog.cpp
+ tutorialcontrol.cpp
+ tutorialmanager.cpp
+ # widgets
+ expanderwidget.cpp
+ finddialog.cpp
+ lineeditclear.cpp
+ linklabel.cpp
+ questionboxmemory.cpp
+ sortabletreewidget.cpp
+ taskprogressmanager.cpp
+ textviewer.cpp
+ widgetutility.cpp
+ filterwidget.cpp
+)
+
+add_library(uibase SHARED
+ ${UIBASE_SOURCES}
+ ${root_headers}
+ ${interface_headers}
+ ${tutorial_headers}
+ ${widget_headers}
+ ${game_features_header}
+ ${formatters_header}
+ pch.h
+ finddialog.ui
+ questionboxmemory.ui
+ taskdialog.ui
+ textviewer.ui
+)
+
+set_target_properties(uibase PROPERTIES
+ DEBUG_POSTFIX d
+ AUTOMOC ON
+ AUTOUIC ON
+ CXX_STANDARD 23
+ CXX_STANDARD_REQUIRED ON
+ CXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
+ POSITION_INDEPENDENT_CODE ON
+)
+
+target_include_directories(uibase
+ PUBLIC
+ $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
+ PRIVATE
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
+)
+
+add_library(mo2::uibase ALIAS uibase)
+
+target_compile_definitions(uibase PRIVATE UIBASE_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)
+
+# installation
+install(TARGETS uibase EXPORT uibaseTargets
+ LIBRARY DESTINATION lib
+ ARCHIVE DESTINATION lib
+ RUNTIME DESTINATION bin
+)
+install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/uibase DESTINATION include)
+install(EXPORT uibaseTargets
+ FILE mo2-uibase-targets.cmake
+ NAMESPACE mo2::
+ DESTINATION lib/cmake/mo2-uibase
+)
diff --git a/libs/uibase/src/delayedfilewriter.cpp b/libs/uibase/src/delayedfilewriter.cpp
new file mode 100644
index 0000000..9d4703a
--- /dev/null
+++ b/libs/uibase/src/delayedfilewriter.cpp
@@ -0,0 +1,49 @@
+#include <uibase/delayedfilewriter.h>
+#include <uibase/log.h>
+
+using namespace MOBase;
+
+DelayedFileWriterBase::DelayedFileWriterBase(int delay) : m_TimerDelay(delay), m_Timer()
+{
+ QObject::connect(&m_Timer, &QTimer::timeout, this, &DelayedFileWriter::timerExpired);
+ m_Timer.setSingleShot(true);
+}
+
+DelayedFileWriterBase::~DelayedFileWriterBase()
+{
+ if (m_Timer.isActive()) {
+ log::error("delayed file save timer active at shutdown");
+ }
+}
+
+void DelayedFileWriterBase::write()
+{
+ m_Timer.start(m_TimerDelay);
+}
+
+void DelayedFileWriterBase::cancel()
+{
+ m_Timer.stop();
+}
+
+void DelayedFileWriterBase::writeImmediately(bool ifOnTimer)
+{
+ if (!ifOnTimer || m_Timer.isActive()) {
+ m_Timer.stop();
+ doWrite();
+ }
+}
+
+void DelayedFileWriterBase::timerExpired()
+{
+ doWrite();
+}
+
+DelayedFileWriter::DelayedFileWriter(DelayedFileWriter::WriterFunc func, int delay)
+ : DelayedFileWriterBase(delay), m_Func(func)
+{}
+
+void DelayedFileWriter::doWrite()
+{
+ m_Func();
+}
diff --git a/libs/uibase/src/diagnosisreport.cpp b/libs/uibase/src/diagnosisreport.cpp
new file mode 100644
index 0000000..8a19a55
--- /dev/null
+++ b/libs/uibase/src/diagnosisreport.cpp
@@ -0,0 +1,25 @@
+/*
+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
+*/
+
+#include <uibase/diagnosisreport.h>
+
+namespace MOBase
+{
+} // namespace MOBase
diff --git a/libs/uibase/src/errorcodes.cpp b/libs/uibase/src/errorcodes.cpp
new file mode 100644
index 0000000..16c262a
--- /dev/null
+++ b/libs/uibase/src/errorcodes.cpp
@@ -0,0 +1,2361 @@
+#include <uibase/errorcodes.h>
+#include <algorithm>
+#include <iterator>
+
+namespace MOBase
+{
+
+struct CodeName
+{
+ DWORD code = 0;
+ const wchar_t* name = nullptr;
+};
+
+static const CodeName g_codes[] = {
+ {1, L"ERROR_INVALID_FUNCTION"},
+ {2, L"ERROR_FILE_NOT_FOUND"},
+ {3, L"ERROR_PATH_NOT_FOUND"},
+ {4, L"ERROR_TOO_MANY_OPEN_FILES"},
+ {5, L"ERROR_ACCESS_DENIED"},
+ {6, L"ERROR_INVALID_HANDLE"},
+ {7, L"ERROR_ARENA_TRASHED"},
+ {8, L"ERROR_NOT_ENOUGH_MEMORY"},
+ {9, L"ERROR_INVALID_BLOCK"},
+ {10, L"ERROR_BAD_ENVIRONMENT"},
+ {11, L"ERROR_BAD_FORMAT"},
+ {12, L"ERROR_INVALID_ACCESS"},
+ {13, L"ERROR_INVALID_DATA"},
+ {14, L"ERROR_OUTOFMEMORY"},
+ {15, L"ERROR_INVALID_DRIVE"},
+ {16, L"ERROR_CURRENT_DIRECTORY"},
+ {17, L"ERROR_NOT_SAME_DEVICE"},
+ {18, L"ERROR_NO_MORE_FILES"},
+ {19, L"ERROR_WRITE_PROTECT"},
+ {20, L"ERROR_BAD_UNIT"},
+ {21, L"ERROR_NOT_READY"},
+ {22, L"ERROR_BAD_COMMAND"},
+ {23, L"ERROR_CRC"},
+ {24, L"ERROR_BAD_LENGTH"},
+ {25, L"ERROR_SEEK"},
+ {26, L"ERROR_NOT_DOS_DISK"},
+ {27, L"ERROR_SECTOR_NOT_FOUND"},
+ {28, L"ERROR_OUT_OF_PAPER"},
+ {29, L"ERROR_WRITE_FAULT"},
+ {30, L"ERROR_READ_FAULT"},
+ {31, L"ERROR_GEN_FAILURE"},
+ {32, L"ERROR_SHARING_VIOLATION"},
+ {33, L"ERROR_LOCK_VIOLATION"},
+ {34, L"ERROR_WRONG_DISK"},
+ {36, L"ERROR_SHARING_BUFFER_EXCEEDED"},
+ {38, L"ERROR_HANDLE_EOF"},
+ {39, L"ERROR_HANDLE_DISK_FULL"},
+ {50, L"ERROR_NOT_SUPPORTED"},
+ {51, L"ERROR_REM_NOT_LIST"},
+ {52, L"ERROR_DUP_NAME"},
+ {53, L"ERROR_BAD_NETPATH"},
+ {54, L"ERROR_NETWORK_BUSY"},
+ {55, L"ERROR_DEV_NOT_EXIST"},
+ {56, L"ERROR_TOO_MANY_CMDS"},
+ {57, L"ERROR_ADAP_HDW_ERR"},
+ {58, L"ERROR_BAD_NET_RESP"},
+ {59, L"ERROR_UNEXP_NET_ERR"},
+ {60, L"ERROR_BAD_REM_ADAP"},
+ {61, L"ERROR_PRINTQ_FULL"},
+ {62, L"ERROR_NO_SPOOL_SPACE"},
+ {63, L"ERROR_PRINT_CANCELLED"},
+ {64, L"ERROR_NETNAME_DELETED"},
+ {65, L"ERROR_NETWORK_ACCESS_DENIED"},
+ {66, L"ERROR_BAD_DEV_TYPE"},
+ {67, L"ERROR_BAD_NET_NAME"},
+ {68, L"ERROR_TOO_MANY_NAMES"},
+ {69, L"ERROR_TOO_MANY_SESS"},
+ {70, L"ERROR_SHARING_PAUSED"},
+ {71, L"ERROR_REQ_NOT_ACCEP"},
+ {72, L"ERROR_REDIR_PAUSED"},
+ {80, L"ERROR_FILE_EXISTS"},
+ {82, L"ERROR_CANNOT_MAKE"},
+ {83, L"ERROR_FAIL_I24"},
+ {84, L"ERROR_OUT_OF_STRUCTURES"},
+ {85, L"ERROR_ALREADY_ASSIGNED"},
+ {86, L"ERROR_INVALID_PASSWORD"},
+ {87, L"ERROR_INVALID_PARAMETER"},
+ {88, L"ERROR_NET_WRITE_FAULT"},
+ {89, L"ERROR_NO_PROC_SLOTS"},
+ {100, L"ERROR_TOO_MANY_SEMAPHORES"},
+ {101, L"ERROR_EXCL_SEM_ALREADY_OWNED"},
+ {102, L"ERROR_SEM_IS_SET"},
+ {103, L"ERROR_TOO_MANY_SEM_REQUESTS"},
+ {104, L"ERROR_INVALID_AT_INTERRUPT_TIME"},
+ {105, L"ERROR_SEM_OWNER_DIED"},
+ {106, L"ERROR_SEM_USER_LIMIT"},
+ {107, L"ERROR_DISK_CHANGE"},
+ {108, L"ERROR_DRIVE_LOCKED"},
+ {109, L"ERROR_BROKEN_PIPE"},
+ {110, L"ERROR_OPEN_FAILED"},
+ {111, L"ERROR_BUFFER_OVERFLOW"},
+ {112, L"ERROR_DISK_FULL"},
+ {113, L"ERROR_NO_MORE_SEARCH_HANDLES"},
+ {114, L"ERROR_INVALID_TARGET_HANDLE"},
+ {117, L"ERROR_INVALID_CATEGORY"},
+ {118, L"ERROR_INVALID_VERIFY_SWITCH"},
+ {119, L"ERROR_BAD_DRIVER_LEVEL"},
+ {120, L"ERROR_CALL_NOT_IMPLEMENTED"},
+ {121, L"ERROR_SEM_TIMEOUT"},
+ {122, L"ERROR_INSUFFICIENT_BUFFER"},
+ {123, L"ERROR_INVALID_NAME"},
+ {124, L"ERROR_INVALID_LEVEL"},
+ {125, L"ERROR_NO_VOLUME_LABEL"},
+ {126, L"ERROR_MOD_NOT_FOUND"},
+ {127, L"ERROR_PROC_NOT_FOUND"},
+ {128, L"ERROR_WAIT_NO_CHILDREN"},
+ {129, L"ERROR_CHILD_NOT_COMPLETE"},
+ {130, L"ERROR_DIRECT_ACCESS_HANDLE"},
+ {131, L"ERROR_NEGATIVE_SEEK"},
+ {132, L"ERROR_SEEK_ON_DEVICE"},
+ {133, L"ERROR_IS_JOIN_TARGET"},
+ {134, L"ERROR_IS_JOINED"},
+ {135, L"ERROR_IS_SUBSTED"},
+ {136, L"ERROR_NOT_JOINED"},
+ {137, L"ERROR_NOT_SUBSTED"},
+ {138, L"ERROR_JOIN_TO_JOIN"},
+ {139, L"ERROR_SUBST_TO_SUBST"},
+ {140, L"ERROR_JOIN_TO_SUBST"},
+ {141, L"ERROR_SUBST_TO_JOIN"},
+ {142, L"ERROR_BUSY_DRIVE"},
+ {143, L"ERROR_SAME_DRIVE"},
+ {144, L"ERROR_DIR_NOT_ROOT"},
+ {145, L"ERROR_DIR_NOT_EMPTY"},
+ {146, L"ERROR_IS_SUBST_PATH"},
+ {147, L"ERROR_IS_JOIN_PATH"},
+ {148, L"ERROR_PATH_BUSY"},
+ {149, L"ERROR_IS_SUBST_TARGET"},
+ {150, L"ERROR_SYSTEM_TRACE"},
+ {151, L"ERROR_INVALID_EVENT_COUNT"},
+ {152, L"ERROR_TOO_MANY_MUXWAITERS"},
+ {153, L"ERROR_INVALID_LIST_FORMAT"},
+ {154, L"ERROR_LABEL_TOO_LONG"},
+ {155, L"ERROR_TOO_MANY_TCBS"},
+ {156, L"ERROR_SIGNAL_REFUSED"},
+ {157, L"ERROR_DISCARDED"},
+ {158, L"ERROR_NOT_LOCKED"},
+ {159, L"ERROR_BAD_THREADID_ADDR"},
+ {160, L"ERROR_BAD_ARGUMENTS"},
+ {161, L"ERROR_BAD_PATHNAME"},
+ {162, L"ERROR_SIGNAL_PENDING"},
+ {164, L"ERROR_MAX_THRDS_REACHED"},
+ {167, L"ERROR_LOCK_FAILED"},
+ {170, L"ERROR_BUSY"},
+ {171, L"ERROR_DEVICE_SUPPORT_IN_PROGRESS"},
+ {173, L"ERROR_CANCEL_VIOLATION"},
+ {174, L"ERROR_ATOMIC_LOCKS_NOT_SUPPORTED"},
+ {180, L"ERROR_INVALID_SEGMENT_NUMBER"},
+ {182, L"ERROR_INVALID_ORDINAL"},
+ {183, L"ERROR_ALREADY_EXISTS"},
+ {186, L"ERROR_INVALID_FLAG_NUMBER"},
+ {187, L"ERROR_SEM_NOT_FOUND"},
+ {188, L"ERROR_INVALID_STARTING_CODESEG"},
+ {189, L"ERROR_INVALID_STACKSEG"},
+ {190, L"ERROR_INVALID_MODULETYPE"},
+ {191, L"ERROR_INVALID_EXE_SIGNATURE"},
+ {192, L"ERROR_EXE_MARKED_INVALID"},
+ {193, L"ERROR_BAD_EXE_FORMAT"},
+ {194, L"ERROR_ITERATED_DATA_EXCEEDS_64k"},
+ {195, L"ERROR_INVALID_MINALLOCSIZE"},
+ {196, L"ERROR_DYNLINK_FROM_INVALID_RING"},
+ {197, L"ERROR_IOPL_NOT_ENABLED"},
+ {198, L"ERROR_INVALID_SEGDPL"},
+ {199, L"ERROR_AUTODATASEG_EXCEEDS_64k"},
+ {200, L"ERROR_RING2SEG_MUST_BE_MOVABLE"},
+ {201, L"ERROR_RELOC_CHAIN_XEEDS_SEGLIM"},
+ {202, L"ERROR_INFLOOP_IN_RELOC_CHAIN"},
+ {203, L"ERROR_ENVVAR_NOT_FOUND"},
+ {205, L"ERROR_NO_SIGNAL_SENT"},
+ {206, L"ERROR_FILENAME_EXCED_RANGE"},
+ {207, L"ERROR_RING2_STACK_IN_USE"},
+ {208, L"ERROR_META_EXPANSION_TOO_LONG"},
+ {209, L"ERROR_INVALID_SIGNAL_NUMBER"},
+ {210, L"ERROR_THREAD_1_INACTIVE"},
+ {212, L"ERROR_LOCKED"},
+ {214, L"ERROR_TOO_MANY_MODULES"},
+ {215, L"ERROR_NESTING_NOT_ALLOWED"},
+ {216, L"ERROR_EXE_MACHINE_TYPE_MISMATCH"},
+ {217, L"ERROR_EXE_CANNOT_MODIFY_SIGNED_BINARY"},
+ {218, L"ERROR_EXE_CANNOT_MODIFY_STRONG_SIGNED_BINARY"},
+ {220, L"ERROR_FILE_CHECKED_OUT"},
+ {221, L"ERROR_CHECKOUT_REQUIRED"},
+ {222, L"ERROR_BAD_FILE_TYPE"},
+ {223, L"ERROR_FILE_TOO_LARGE"},
+ {224, L"ERROR_FORMS_AUTH_REQUIRED"},
+ {225, L"ERROR_VIRUS_INFECTED"},
+ {226, L"ERROR_VIRUS_DELETED"},
+ {229, L"ERROR_PIPE_LOCAL"},
+ {230, L"ERROR_BAD_PIPE"},
+ {231, L"ERROR_PIPE_BUSY"},
+ {232, L"ERROR_NO_DATA"},
+ {233, L"ERROR_PIPE_NOT_CONNECTED"},
+ {234, L"ERROR_MORE_DATA"},
+ {240, L"ERROR_VC_DISCONNECTED"},
+ {254, L"ERROR_INVALID_EA_NAME"},
+ {255, L"ERROR_EA_LIST_INCONSISTENT"},
+ {258, L"WAIT_TIMEOUT"},
+ {259, L"ERROR_NO_MORE_ITEMS"},
+ {266, L"ERROR_CANNOT_COPY"},
+ {267, L"ERROR_DIRECTORY"},
+ {275, L"ERROR_EAS_DIDNT_FIT"},
+ {276, L"ERROR_EA_FILE_CORRUPT"},
+ {277, L"ERROR_EA_TABLE_FULL"},
+ {278, L"ERROR_INVALID_EA_HANDLE"},
+ {282, L"ERROR_EAS_NOT_SUPPORTED"},
+ {288, L"ERROR_NOT_OWNER"},
+ {298, L"ERROR_TOO_MANY_POSTS"},
+ {299, L"ERROR_PARTIAL_COPY"},
+ {300, L"ERROR_OPLOCK_NOT_GRANTED"},
+ {301, L"ERROR_INVALID_OPLOCK_PROTOCOL"},
+ {302, L"ERROR_DISK_TOO_FRAGMENTED"},
+ {303, L"ERROR_DELETE_PENDING"},
+ {304, L"ERROR_INCOMPATIBLE_WITH_GLOBAL_SHORT_NAME_REGISTRY_SETTING"},
+ {305, L"ERROR_SHORT_NAMES_NOT_ENABLED_ON_VOLUME"},
+ {306, L"ERROR_SECURITY_STREAM_IS_INCONSISTENT"},
+ {307, L"ERROR_INVALID_LOCK_RANGE"},
+ {308, L"ERROR_IMAGE_SUBSYSTEM_NOT_PRESENT"},
+ {309, L"ERROR_NOTIFICATION_GUID_ALREADY_DEFINED"},
+ {310, L"ERROR_INVALID_EXCEPTION_HANDLER"},
+ {311, L"ERROR_DUPLICATE_PRIVILEGES"},
+ {312, L"ERROR_NO_RANGES_PROCESSED"},
+ {313, L"ERROR_NOT_ALLOWED_ON_SYSTEM_FILE"},
+ {314, L"ERROR_DISK_RESOURCES_EXHAUSTED"},
+ {315, L"ERROR_INVALID_TOKEN"},
+ {316, L"ERROR_DEVICE_FEATURE_NOT_SUPPORTED"},
+ {317, L"ERROR_MR_MID_NOT_FOUND"},
+ {318, L"ERROR_SCOPE_NOT_FOUND"},
+ {319, L"ERROR_UNDEFINED_SCOPE"},
+ {320, L"ERROR_INVALID_CAP"},
+ {321, L"ERROR_DEVICE_UNREACHABLE"},
+ {322, L"ERROR_DEVICE_NO_RESOURCES"},
+ {323, L"ERROR_DATA_CHECKSUM_ERROR"},
+ {324, L"ERROR_INTERMIXED_KERNEL_EA_OPERATION"},
+ {326, L"ERROR_FILE_LEVEL_TRIM_NOT_SUPPORTED"},
+ {327, L"ERROR_OFFSET_ALIGNMENT_VIOLATION"},
+ {328, L"ERROR_INVALID_FIELD_IN_PARAMETER_LIST"},
+ {329, L"ERROR_OPERATION_IN_PROGRESS"},
+ {330, L"ERROR_BAD_DEVICE_PATH"},
+ {331, L"ERROR_TOO_MANY_DESCRIPTORS"},
+ {332, L"ERROR_SCRUB_DATA_DISABLED"},
+ {333, L"ERROR_NOT_REDUNDANT_STORAGE"},
+ {334, L"ERROR_RESIDENT_FILE_NOT_SUPPORTED"},
+ {335, L"ERROR_COMPRESSED_FILE_NOT_SUPPORTED"},
+ {336, L"ERROR_DIRECTORY_NOT_SUPPORTED"},
+ {337, L"ERROR_NOT_READ_FROM_COPY"},
+ {350, L"ERROR_FAIL_NOACTION_REBOOT"},
+ {351, L"ERROR_FAIL_SHUTDOWN"},
+ {352, L"ERROR_FAIL_RESTART"},
+ {353, L"ERROR_MAX_SESSIONS_REACHED"},
+ {400, L"ERROR_THREAD_MODE_ALREADY_BACKGROUND"},
+ {401, L"ERROR_THREAD_MODE_NOT_BACKGROUND"},
+ {402, L"ERROR_PROCESS_MODE_ALREADY_BACKGROUND"},
+ {403, L"ERROR_PROCESS_MODE_NOT_BACKGROUND"},
+ {487, L"ERROR_INVALID_ADDRESS"},
+ {500, L"ERROR_USER_PROFILE_LOAD"},
+ {534, L"ERROR_ARITHMETIC_OVERFLOW"},
+ {535, L"ERROR_PIPE_CONNECTED"},
+ {536, L"ERROR_PIPE_LISTENING"},
+ {537, L"ERROR_VERIFIER_STOP"},
+ {538, L"ERROR_ABIOS_ERROR"},
+ {539, L"ERROR_WX86_WARNING"},
+ {540, L"ERROR_WX86_ERROR"},
+ {541, L"ERROR_TIMER_NOT_CANCELED"},
+ {542, L"ERROR_UNWIND"},
+ {543, L"ERROR_BAD_STACK"},
+ {544, L"ERROR_INVALID_UNWIND_TARGET"},
+ {545, L"ERROR_INVALID_PORT_ATTRIBUTES"},
+ {546, L"ERROR_PORT_MESSAGE_TOO_LONG"},
+ {547, L"ERROR_INVALID_QUOTA_LOWER"},
+ {548, L"ERROR_DEVICE_ALREADY_ATTACHED"},
+ {549, L"ERROR_INSTRUCTION_MISALIGNMENT"},
+ {550, L"ERROR_PROFILING_NOT_STARTED"},
+ {551, L"ERROR_PROFILING_NOT_STOPPED"},
+ {552, L"ERROR_COULD_NOT_INTERPRET"},
+ {553, L"ERROR_PROFILING_AT_LIMIT"},
+ {554, L"ERROR_CANT_WAIT"},
+ {555, L"ERROR_CANT_TERMINATE_SELF"},
+ {556, L"ERROR_UNEXPECTED_MM_CREATE_ERR"},
+ {557, L"ERROR_UNEXPECTED_MM_MAP_ERROR"},
+ {558, L"ERROR_UNEXPECTED_MM_EXTEND_ERR"},
+ {559, L"ERROR_BAD_FUNCTION_TABLE"},
+ {560, L"ERROR_NO_GUID_TRANSLATION"},
+ {561, L"ERROR_INVALID_LDT_SIZE"},
+ {563, L"ERROR_INVALID_LDT_OFFSET"},
+ {564, L"ERROR_INVALID_LDT_DESCRIPTOR"},
+ {565, L"ERROR_TOO_MANY_THREADS"},
+ {566, L"ERROR_THREAD_NOT_IN_PROCESS"},
+ {567, L"ERROR_PAGEFILE_QUOTA_EXCEEDED"},
+ {568, L"ERROR_LOGON_SERVER_CONFLICT"},
+ {569, L"ERROR_SYNCHRONIZATION_REQUIRED"},
+ {570, L"ERROR_NET_OPEN_FAILED"},
+ {571, L"ERROR_IO_PRIVILEGE_FAILED"},
+ {572, L"ERROR_CONTROL_C_EXIT"},
+ {573, L"ERROR_MISSING_SYSTEMFILE"},
+ {574, L"ERROR_UNHANDLED_EXCEPTION"},
+ {575, L"ERROR_APP_INIT_FAILURE"},
+ {576, L"ERROR_PAGEFILE_CREATE_FAILED"},
+ {577, L"ERROR_INVALID_IMAGE_HASH"},
+ {578, L"ERROR_NO_PAGEFILE"},
+ {579, L"ERROR_ILLEGAL_FLOAT_CONTEXT"},
+ {580, L"ERROR_NO_EVENT_PAIR"},
+ {581, L"ERROR_DOMAIN_CTRLR_CONFIG_ERROR"},
+ {582, L"ERROR_ILLEGAL_CHARACTER"},
+ {583, L"ERROR_UNDEFINED_CHARACTER"},
+ {584, L"ERROR_FLOPPY_VOLUME"},
+ {585, L"ERROR_BIOS_FAILED_TO_CONNECT_INTERRUPT"},
+ {586, L"ERROR_BACKUP_CONTROLLER"},
+ {587, L"ERROR_MUTANT_LIMIT_EXCEEDED"},
+ {588, L"ERROR_FS_DRIVER_REQUIRED"},
+ {589, L"ERROR_CANNOT_LOAD_REGISTRY_FILE"},
+ {590, L"ERROR_DEBUG_ATTACH_FAILED"},
+ {591, L"ERROR_SYSTEM_PROCESS_TERMINATED"},
+ {592, L"ERROR_DATA_NOT_ACCEPTED"},
+ {593, L"ERROR_VDM_HARD_ERROR"},
+ {594, L"ERROR_DRIVER_CANCEL_TIMEOUT"},
+ {595, L"ERROR_REPLY_MESSAGE_MISMATCH"},
+ {596, L"ERROR_LOST_WRITEBEHIND_DATA"},
+ {597, L"ERROR_CLIENT_SERVER_PARAMETERS_INVALID"},
+ {598, L"ERROR_NOT_TINY_STREAM"},
+ {599, L"ERROR_STACK_OVERFLOW_READ"},
+ {600, L"ERROR_CONVERT_TO_LARGE"},
+ {601, L"ERROR_FOUND_OUT_OF_SCOPE"},
+ {602, L"ERROR_ALLOCATE_BUCKET"},
+ {603, L"ERROR_MARSHALL_OVERFLOW"},
+ {604, L"ERROR_INVALID_VARIANT"},
+ {605, L"ERROR_BAD_COMPRESSION_BUFFER"},
+ {606, L"ERROR_AUDIT_FAILED"},
+ {607, L"ERROR_TIMER_RESOLUTION_NOT_SET"},
+ {608, L"ERROR_INSUFFICIENT_LOGON_INFO"},
+ {609, L"ERROR_BAD_DLL_ENTRYPOINT"},
+ {610, L"ERROR_BAD_SERVICE_ENTRYPOINT"},
+ {611, L"ERROR_IP_ADDRESS_CONFLICT1"},
+ {612, L"ERROR_IP_ADDRESS_CONFLICT2"},
+ {613, L"ERROR_REGISTRY_QUOTA_LIMIT"},
+ {614, L"ERROR_NO_CALLBACK_ACTIVE"},
+ {615, L"ERROR_PWD_TOO_SHORT"},
+ {616, L"ERROR_PWD_TOO_RECENT"},
+ {617, L"ERROR_PWD_HISTORY_CONFLICT"},
+ {618, L"ERROR_UNSUPPORTED_COMPRESSION"},
+ {619, L"ERROR_INVALID_HW_PROFILE"},
+ {620, L"ERROR_INVALID_PLUGPLAY_DEVICE_PATH"},
+ {621, L"ERROR_QUOTA_LIST_INCONSISTENT"},
+ {622, L"ERROR_EVALUATION_EXPIRATION"},
+ {623, L"ERROR_ILLEGAL_DLL_RELOCATION"},
+ {624, L"ERROR_DLL_INIT_FAILED_LOGOFF"},
+ {625, L"ERROR_VALIDATE_CONTINUE"},
+ {626, L"ERROR_NO_MORE_MATCHES"},
+ {627, L"ERROR_RANGE_LIST_CONFLICT"},
+ {628, L"ERROR_SERVER_SID_MISMATCH"},
+ {629, L"ERROR_CANT_ENABLE_DENY_ONLY"},
+ {630, L"ERROR_FLOAT_MULTIPLE_FAULTS"},
+ {631, L"ERROR_FLOAT_MULTIPLE_TRAPS"},
+ {632, L"ERROR_NOINTERFACE"},
+ {633, L"ERROR_DRIVER_FAILED_SLEEP"},
+ {634, L"ERROR_CORRUPT_SYSTEM_FILE"},
+ {635, L"ERROR_COMMITMENT_MINIMUM"},
+ {636, L"ERROR_PNP_RESTART_ENUMERATION"},
+ {637, L"ERROR_SYSTEM_IMAGE_BAD_SIGNATURE"},
+ {638, L"ERROR_PNP_REBOOT_REQUIRED"},
+ {639, L"ERROR_INSUFFICIENT_POWER"},
+ {640, L"ERROR_MULTIPLE_FAULT_VIOLATION"},
+ {641, L"ERROR_SYSTEM_SHUTDOWN"},
+ {642, L"ERROR_PORT_NOT_SET"},
+ {643, L"ERROR_DS_VERSION_CHECK_FAILURE"},
+ {644, L"ERROR_RANGE_NOT_FOUND"},
+ {646, L"ERROR_NOT_SAFE_MODE_DRIVER"},
+ {647, L"ERROR_FAILED_DRIVER_ENTRY"},
+ {648, L"ERROR_DEVICE_ENUMERATION_ERROR"},
+ {649, L"ERROR_MOUNT_POINT_NOT_RESOLVED"},
+ {650, L"ERROR_INVALID_DEVICE_OBJECT_PARAMETER"},
+ {651, L"ERROR_MCA_OCCURED"},
+ {652, L"ERROR_DRIVER_DATABASE_ERROR"},
+ {653, L"ERROR_SYSTEM_HIVE_TOO_LARGE"},
+ {654, L"ERROR_DRIVER_FAILED_PRIOR_UNLOAD"},
+ {655, L"ERROR_VOLSNAP_PREPARE_HIBERNATE"},
+ {656, L"ERROR_HIBERNATION_FAILURE"},
+ {657, L"ERROR_PWD_TOO_LONG"},
+ {665, L"ERROR_FILE_SYSTEM_LIMITATION"},
+ {668, L"ERROR_ASSERTION_FAILURE"},
+ {669, L"ERROR_ACPI_ERROR"},
+ {670, L"ERROR_WOW_ASSERTION"},
+ {671, L"ERROR_PNP_BAD_MPS_TABLE"},
+ {672, L"ERROR_PNP_TRANSLATION_FAILED"},
+ {673, L"ERROR_PNP_IRQ_TRANSLATION_FAILED"},
+ {674, L"ERROR_PNP_INVALID_ID"},
+ {675, L"ERROR_WAKE_SYSTEM_DEBUGGER"},
+ {676, L"ERROR_HANDLES_CLOSED"},
+ {677, L"ERROR_EXTRANEOUS_INFORMATION"},
+ {678, L"ERROR_RXACT_COMMIT_NECESSARY"},
+ {679, L"ERROR_MEDIA_CHECK"},
+ {680, L"ERROR_GUID_SUBSTITUTION_MADE"},
+ {681, L"ERROR_STOPPED_ON_SYMLINK"},
+ {682, L"ERROR_LONGJUMP"},
+ {683, L"ERROR_PLUGPLAY_QUERY_VETOED"},
+ {684, L"ERROR_UNWIND_CONSOLIDATE"},
+ {685, L"ERROR_REGISTRY_HIVE_RECOVERED"},
+ {686, L"ERROR_DLL_MIGHT_BE_INSECURE"},
+ {687, L"ERROR_DLL_MIGHT_BE_INCOMPATIBLE"},
+ {688, L"ERROR_DBG_EXCEPTION_NOT_HANDLED"},
+ {689, L"ERROR_DBG_REPLY_LATER"},
+ {690, L"ERROR_DBG_UNABLE_TO_PROVIDE_HANDLE"},
+ {691, L"ERROR_DBG_TERMINATE_THREAD"},
+ {692, L"ERROR_DBG_TERMINATE_PROCESS"},
+ {693, L"ERROR_DBG_CONTROL_C"},
+ {694, L"ERROR_DBG_PRINTEXCEPTION_C"},
+ {695, L"ERROR_DBG_RIPEXCEPTION"},
+ {696, L"ERROR_DBG_CONTROL_BREAK"},
+ {697, L"ERROR_DBG_COMMAND_EXCEPTION"},
+ {698, L"ERROR_OBJECT_NAME_EXISTS"},
+ {699, L"ERROR_THREAD_WAS_SUSPENDED"},
+ {700, L"ERROR_IMAGE_NOT_AT_BASE"},
+ {701, L"ERROR_RXACT_STATE_CREATED"},
+ {702, L"ERROR_SEGMENT_NOTIFICATION"},
+ {703, L"ERROR_BAD_CURRENT_DIRECTORY"},
+ {704, L"ERROR_FT_READ_RECOVERY_FROM_BACKUP"},
+ {705, L"ERROR_FT_WRITE_RECOVERY"},
+ {706, L"ERROR_IMAGE_MACHINE_TYPE_MISMATCH"},
+ {707, L"ERROR_RECEIVE_PARTIAL"},
+ {708, L"ERROR_RECEIVE_EXPEDITED"},
+ {709, L"ERROR_RECEIVE_PARTIAL_EXPEDITED"},
+ {710, L"ERROR_EVENT_DONE"},
+ {711, L"ERROR_EVENT_PENDING"},
+ {712, L"ERROR_CHECKING_FILE_SYSTEM"},
+ {713, L"ERROR_FATAL_APP_EXIT"},
+ {714, L"ERROR_PREDEFINED_HANDLE"},
+ {715, L"ERROR_WAS_UNLOCKED"},
+ {716, L"ERROR_SERVICE_NOTIFICATION"},
+ {717, L"ERROR_WAS_LOCKED"},
+ {718, L"ERROR_LOG_HARD_ERROR"},
+ {719, L"ERROR_ALREADY_WIN32"},
+ {720, L"ERROR_IMAGE_MACHINE_TYPE_MISMATCH_EXE"},
+ {721, L"ERROR_NO_YIELD_PERFORMED"},
+ {722, L"ERROR_TIMER_RESUME_IGNORED"},
+ {723, L"ERROR_ARBITRATION_UNHANDLED"},
+ {724, L"ERROR_CARDBUS_NOT_SUPPORTED"},
+ {725, L"ERROR_MP_PROCESSOR_MISMATCH"},
+ {726, L"ERROR_HIBERNATED"},
+ {727, L"ERROR_RESUME_HIBERNATION"},
+ {728, L"ERROR_FIRMWARE_UPDATED"},
+ {729, L"ERROR_DRIVERS_LEAKING_LOCKED_PAGES"},
+ {730, L"ERROR_WAKE_SYSTEM"},
+ {731, L"ERROR_WAIT_1"},
+ {732, L"ERROR_WAIT_2"},
+ {733, L"ERROR_WAIT_3"},
+ {734, L"ERROR_WAIT_63"},
+ {735, L"ERROR_ABANDONED_WAIT_0"},
+ {736, L"ERROR_ABANDONED_WAIT_63"},
+ {737, L"ERROR_USER_APC"},
+ {738, L"ERROR_KERNEL_APC"},
+ {739, L"ERROR_ALERTED"},
+ {740, L"ERROR_ELEVATION_REQUIRED"},
+ {741, L"ERROR_REPARSE"},
+ {742, L"ERROR_OPLOCK_BREAK_IN_PROGRESS"},
+ {743, L"ERROR_VOLUME_MOUNTED"},
+ {744, L"ERROR_RXACT_COMMITTED"},
+ {745, L"ERROR_NOTIFY_CLEANUP"},
+ {746, L"ERROR_PRIMARY_TRANSPORT_CONNECT_FAILED"},
+ {747, L"ERROR_PAGE_FAULT_TRANSITION"},
+ {748, L"ERROR_PAGE_FAULT_DEMAND_ZERO"},
+ {749, L"ERROR_PAGE_FAULT_COPY_ON_WRITE"},
+ {750, L"ERROR_PAGE_FAULT_GUARD_PAGE"},
+ {751, L"ERROR_PAGE_FAULT_PAGING_FILE"},
+ {752, L"ERROR_CACHE_PAGE_LOCKED"},
+ {753, L"ERROR_CRASH_DUMP"},
+ {754, L"ERROR_BUFFER_ALL_ZEROS"},
+ {755, L"ERROR_REPARSE_OBJECT"},
+ {756, L"ERROR_RESOURCE_REQUIREMENTS_CHANGED"},
+ {757, L"ERROR_TRANSLATION_COMPLETE"},
+ {758, L"ERROR_NOTHING_TO_TERMINATE"},
+ {759, L"ERROR_PROCESS_NOT_IN_JOB"},
+ {760, L"ERROR_PROCESS_IN_JOB"},
+ {761, L"ERROR_VOLSNAP_HIBERNATE_READY"},
+ {762, L"ERROR_FSFILTER_OP_COMPLETED_SUCCESSFULLY"},
+ {763, L"ERROR_INTERRUPT_VECTOR_ALREADY_CONNECTED"},
+ {764, L"ERROR_INTERRUPT_STILL_CONNECTED"},
+ {765, L"ERROR_WAIT_FOR_OPLOCK"},
+ {766, L"ERROR_DBG_EXCEPTION_HANDLED"},
+ {767, L"ERROR_DBG_CONTINUE"},
+ {768, L"ERROR_CALLBACK_POP_STACK"},
+ {769, L"ERROR_COMPRESSION_DISABLED"},
+ {770, L"ERROR_CANTFETCHBACKWARDS"},
+ {771, L"ERROR_CANTSCROLLBACKWARDS"},
+ {772, L"ERROR_ROWSNOTRELEASED"},
+ {773, L"ERROR_BAD_ACCESSOR_FLAGS"},
+ {774, L"ERROR_ERRORS_ENCOUNTERED"},
+ {775, L"ERROR_NOT_CAPABLE"},
+ {776, L"ERROR_REQUEST_OUT_OF_SEQUENCE"},
+ {777, L"ERROR_VERSION_PARSE_ERROR"},
+ {778, L"ERROR_BADSTARTPOSITION"},
+ {779, L"ERROR_MEMORY_HARDWARE"},
+ {780, L"ERROR_DISK_REPAIR_DISABLED"},
+ {781, L"ERROR_INSUFFICIENT_RESOURCE_FOR_SPECIFIED_SHARED_SECTION_SIZE"},
+ {782, L"ERROR_SYSTEM_POWERSTATE_TRANSITION"},
+ {783, L"ERROR_SYSTEM_POWERSTATE_COMPLEX_TRANSITION"},
+ {784, L"ERROR_MCA_EXCEPTION"},
+ {785, L"ERROR_ACCESS_AUDIT_BY_POLICY"},
+ {786, L"ERROR_ACCESS_DISABLED_NO_SAFER_UI_BY_POLICY"},
+ {787, L"ERROR_ABANDON_HIBERFILE"},
+ {788, L"ERROR_LOST_WRITEBEHIND_DATA_NETWORK_DISCONNECTED"},
+ {789, L"ERROR_LOST_WRITEBEHIND_DATA_NETWORK_SERVER_ERROR"},
+ {790, L"ERROR_LOST_WRITEBEHIND_DATA_LOCAL_DISK_ERROR"},
+ {791, L"ERROR_BAD_MCFG_TABLE"},
+ {792, L"ERROR_DISK_REPAIR_REDIRECTED"},
+ {793, L"ERROR_DISK_REPAIR_UNSUCCESSFUL"},
+ {794, L"ERROR_CORRUPT_LOG_OVERFULL"},
+ {795, L"ERROR_CORRUPT_LOG_CORRUPTED"},
+ {796, L"ERROR_CORRUPT_LOG_UNAVAILABLE"},
+ {797, L"ERROR_CORRUPT_LOG_DELETED_FULL"},
+ {798, L"ERROR_CORRUPT_LOG_CLEARED"},
+ {799, L"ERROR_ORPHAN_NAME_EXHAUSTED"},
+ {800, L"ERROR_OPLOCK_SWITCHED_TO_NEW_HANDLE"},
+ {801, L"ERROR_CANNOT_GRANT_REQUESTED_OPLOCK"},
+ {802, L"ERROR_CANNOT_BREAK_OPLOCK"},
+ {803, L"ERROR_OPLOCK_HANDLE_CLOSED"},
+ {804, L"ERROR_NO_ACE_CONDITION"},
+ {805, L"ERROR_INVALID_ACE_CONDITION"},
+ {806, L"ERROR_FILE_HANDLE_REVOKED"},
+ {807, L"ERROR_IMAGE_AT_DIFFERENT_BASE"},
+ {994, L"ERROR_EA_ACCESS_DENIED"},
+ {995, L"ERROR_OPERATION_ABORTED"},
+ {996, L"ERROR_IO_INCOMPLETE"},
+ {997, L"ERROR_IO_PENDING"},
+ {998, L"ERROR_NOACCESS"},
+ {999, L"ERROR_SWAPERROR"},
+ {1001, L"ERROR_STACK_OVERFLOW"},
+ {1002, L"ERROR_INVALID_MESSAGE"},
+ {1003, L"ERROR_CAN_NOT_COMPLETE"},
+ {1004, L"ERROR_INVALID_FLAGS"},
+ {1005, L"ERROR_UNRECOGNIZED_VOLUME"},
+ {1006, L"ERROR_FILE_INVALID"},
+ {1007, L"ERROR_FULLSCREEN_MODE"},
+ {1008, L"ERROR_NO_TOKEN"},
+ {1009, L"ERROR_BADDB"},
+ {1010, L"ERROR_BADKEY"},
+ {1011, L"ERROR_CANTOPEN"},
+ {1012, L"ERROR_CANTREAD"},
+ {1013, L"ERROR_CANTWRITE"},
+ {1014, L"ERROR_REGISTRY_RECOVERED"},
+ {1015, L"ERROR_REGISTRY_CORRUPT"},
+ {1016, L"ERROR_REGISTRY_IO_FAILED"},
+ {1017, L"ERROR_NOT_REGISTRY_FILE"},
+ {1018, L"ERROR_KEY_DELETED"},
+ {1019, L"ERROR_NO_LOG_SPACE"},
+ {1020, L"ERROR_KEY_HAS_CHILDREN"},
+ {1021, L"ERROR_CHILD_MUST_BE_VOLATILE"},
+ {1022, L"ERROR_NOTIFY_ENUM_DIR"},
+ {1051, L"ERROR_DEPENDENT_SERVICES_RUNNING"},
+ {1052, L"ERROR_INVALID_SERVICE_CONTROL"},
+ {1053, L"ERROR_SERVICE_REQUEST_TIMEOUT"},
+ {1054, L"ERROR_SERVICE_NO_THREAD"},
+ {1055, L"ERROR_SERVICE_DATABASE_LOCKED"},
+ {1056, L"ERROR_SERVICE_ALREADY_RUNNING"},
+ {1057, L"ERROR_INVALID_SERVICE_ACCOUNT"},
+ {1058, L"ERROR_SERVICE_DISABLED"},
+ {1059, L"ERROR_CIRCULAR_DEPENDENCY"},
+ {1060, L"ERROR_SERVICE_DOES_NOT_EXIST"},
+ {1061, L"ERROR_SERVICE_CANNOT_ACCEPT_CTRL"},
+ {1062, L"ERROR_SERVICE_NOT_ACTIVE"},
+ {1063, L"ERROR_FAILED_SERVICE_CONTROLLER_CONNECT"},
+ {1064, L"ERROR_EXCEPTION_IN_SERVICE"},
+ {1065, L"ERROR_DATABASE_DOES_NOT_EXIST"},
+ {1066, L"ERROR_SERVICE_SPECIFIC_ERROR"},
+ {1067, L"ERROR_PROCESS_ABORTED"},
+ {1068, L"ERROR_SERVICE_DEPENDENCY_FAIL"},
+ {1069, L"ERROR_SERVICE_LOGON_FAILED"},
+ {1070, L"ERROR_SERVICE_START_HANG"},
+ {1071, L"ERROR_INVALID_SERVICE_LOCK"},
+ {1072, L"ERROR_SERVICE_MARKED_FOR_DELETE"},
+ {1073, L"ERROR_SERVICE_EXISTS"},
+ {1074, L"ERROR_ALREADY_RUNNING_LKG"},
+ {1075, L"ERROR_SERVICE_DEPENDENCY_DELETED"},
+ {1076, L"ERROR_BOOT_ALREADY_ACCEPTED"},
+ {1077, L"ERROR_SERVICE_NEVER_STARTED"},
+ {1078, L"ERROR_DUPLICATE_SERVICE_NAME"},
+ {1079, L"ERROR_DIFFERENT_SERVICE_ACCOUNT"},
+ {1080, L"ERROR_CANNOT_DETECT_DRIVER_FAILURE"},
+ {1081, L"ERROR_CANNOT_DETECT_PROCESS_ABORT"},
+ {1082, L"ERROR_NO_RECOVERY_PROGRAM"},
+ {1083, L"ERROR_SERVICE_NOT_IN_EXE"},
+ {1084, L"ERROR_NOT_SAFEBOOT_SERVICE"},
+ {1100, L"ERROR_END_OF_MEDIA"},
+ {1101, L"ERROR_FILEMARK_DETECTED"},
+ {1102, L"ERROR_BEGINNING_OF_MEDIA"},
+ {1103, L"ERROR_SETMARK_DETECTED"},
+ {1104, L"ERROR_NO_DATA_DETECTED"},
+ {1105, L"ERROR_PARTITION_FAILURE"},
+ {1106, L"ERROR_INVALID_BLOCK_LENGTH"},
+ {1107, L"ERROR_DEVICE_NOT_PARTITIONED"},
+ {1108, L"ERROR_UNABLE_TO_LOCK_MEDIA"},
+ {1109, L"ERROR_UNABLE_TO_UNLOAD_MEDIA"},
+ {1110, L"ERROR_MEDIA_CHANGED"},
+ {1111, L"ERROR_BUS_RESET"},
+ {1112, L"ERROR_NO_MEDIA_IN_DRIVE"},
+ {1113, L"ERROR_NO_UNICODE_TRANSLATION"},
+ {1114, L"ERROR_DLL_INIT_FAILED"},
+ {1115, L"ERROR_SHUTDOWN_IN_PROGRESS"},
+ {1116, L"ERROR_NO_SHUTDOWN_IN_PROGRESS"},
+ {1117, L"ERROR_IO_DEVICE"},
+ {1118, L"ERROR_SERIAL_NO_DEVICE"},
+ {1119, L"ERROR_IRQ_BUSY"},
+ {1120, L"ERROR_MORE_WRITES"},
+ {1121, L"ERROR_COUNTER_TIMEOUT"},
+ {1122, L"ERROR_FLOPPY_ID_MARK_NOT_FOUND"},
+ {1123, L"ERROR_FLOPPY_WRONG_CYLINDER"},
+ {1124, L"ERROR_FLOPPY_UNKNOWN_ERROR"},
+ {1125, L"ERROR_FLOPPY_BAD_REGISTERS"},
+ {1126, L"ERROR_DISK_RECALIBRATE_FAILED"},
+ {1127, L"ERROR_DISK_OPERATION_FAILED"},
+ {1128, L"ERROR_DISK_RESET_FAILED"},
+ {1129, L"ERROR_EOM_OVERFLOW"},
+ {1130, L"ERROR_NOT_ENOUGH_SERVER_MEMORY"},
+ {1131, L"ERROR_POSSIBLE_DEADLOCK"},
+ {1132, L"ERROR_MAPPED_ALIGNMENT"},
+ {1140, L"ERROR_SET_POWER_STATE_VETOED"},
+ {1141, L"ERROR_SET_POWER_STATE_FAILED"},
+ {1142, L"ERROR_TOO_MANY_LINKS"},
+ {1150, L"ERROR_OLD_WIN_VERSION"},
+ {1151, L"ERROR_APP_WRONG_OS"},
+ {1152, L"ERROR_SINGLE_INSTANCE_APP"},
+ {1153, L"ERROR_RMODE_APP"},
+ {1154, L"ERROR_INVALID_DLL"},
+ {1155, L"ERROR_NO_ASSOCIATION"},
+ {1156, L"ERROR_DDE_FAIL"},
+ {1157, L"ERROR_DLL_NOT_FOUND"},
+ {1158, L"ERROR_NO_MORE_USER_HANDLES"},
+ {1159, L"ERROR_MESSAGE_SYNC_ONLY"},
+ {1160, L"ERROR_SOURCE_ELEMENT_EMPTY"},
+ {1161, L"ERROR_DESTINATION_ELEMENT_FULL"},
+ {1162, L"ERROR_ILLEGAL_ELEMENT_ADDRESS"},
+ {1163, L"ERROR_MAGAZINE_NOT_PRESENT"},
+ {1164, L"ERROR_DEVICE_REINITIALIZATION_NEEDED"},
+ {1165, L"ERROR_DEVICE_REQUIRES_CLEANING"},
+ {1166, L"ERROR_DEVICE_DOOR_OPEN"},
+ {1167, L"ERROR_DEVICE_NOT_CONNECTED"},
+ {1168, L"ERROR_NOT_FOUND"},
+ {1169, L"ERROR_NO_MATCH"},
+ {1170, L"ERROR_SET_NOT_FOUND"},
+ {1171, L"ERROR_POINT_NOT_FOUND"},
+ {1172, L"ERROR_NO_TRACKING_SERVICE"},
+ {1173, L"ERROR_NO_VOLUME_ID"},
+ {1175, L"ERROR_UNABLE_TO_REMOVE_REPLACED"},
+ {1176, L"ERROR_UNABLE_TO_MOVE_REPLACEMENT"},
+ {1177, L"ERROR_UNABLE_TO_MOVE_REPLACEMENT_2"},
+ {1178, L"ERROR_JOURNAL_DELETE_IN_PROGRESS"},
+ {1179, L"ERROR_JOURNAL_NOT_ACTIVE"},
+ {1180, L"ERROR_POTENTIAL_FILE_FOUND"},
+ {1181, L"ERROR_JOURNAL_ENTRY_DELETED"},
+ {1190, L"ERROR_SHUTDOWN_IS_SCHEDULED"},
+ {1191, L"ERROR_SHUTDOWN_USERS_LOGGED_ON"},
+ {1200, L"ERROR_BAD_DEVICE"},
+ {1201, L"ERROR_CONNECTION_UNAVAIL"},
+ {1202, L"ERROR_DEVICE_ALREADY_REMEMBERED"},
+ {1203, L"ERROR_NO_NET_OR_BAD_PATH"},
+ {1204, L"ERROR_BAD_PROVIDER"},
+ {1205, L"ERROR_CANNOT_OPEN_PROFILE"},
+ {1206, L"ERROR_BAD_PROFILE"},
+ {1207, L"ERROR_NOT_CONTAINER"},
+ {1208, L"ERROR_EXTENDED_ERROR"},
+ {1209, L"ERROR_INVALID_GROUPNAME"},
+ {1210, L"ERROR_INVALID_COMPUTERNAME"},
+ {1211, L"ERROR_INVALID_EVENTNAME"},
+ {1212, L"ERROR_INVALID_DOMAINNAME"},
+ {1213, L"ERROR_INVALID_SERVICENAME"},
+ {1214, L"ERROR_INVALID_NETNAME"},
+ {1215, L"ERROR_INVALID_SHARENAME"},
+ {1216, L"ERROR_INVALID_PASSWORDNAME"},
+ {1217, L"ERROR_INVALID_MESSAGENAME"},
+ {1218, L"ERROR_INVALID_MESSAGEDEST"},
+ {1219, L"ERROR_SESSION_CREDENTIAL_CONFLICT"},
+ {1220, L"ERROR_REMOTE_SESSION_LIMIT_EXCEEDED"},
+ {1221, L"ERROR_DUP_DOMAINNAME"},
+ {1222, L"ERROR_NO_NETWORK"},
+ {1223, L"ERROR_CANCELLED"},
+ {1224, L"ERROR_USER_MAPPED_FILE"},
+ {1225, L"ERROR_CONNECTION_REFUSED"},
+ {1226, L"ERROR_GRACEFUL_DISCONNECT"},
+ {1227, L"ERROR_ADDRESS_ALREADY_ASSOCIATED"},
+ {1228, L"ERROR_ADDRESS_NOT_ASSOCIATED"},
+ {1229, L"ERROR_CONNECTION_INVALID"},
+ {1230, L"ERROR_CONNECTION_ACTIVE"},
+ {1231, L"ERROR_NETWORK_UNREACHABLE"},
+ {1232, L"ERROR_HOST_UNREACHABLE"},
+ {1233, L"ERROR_PROTOCOL_UNREACHABLE"},
+ {1234, L"ERROR_PORT_UNREACHABLE"},
+ {1235, L"ERROR_REQUEST_ABORTED"},
+ {1236, L"ERROR_CONNECTION_ABORTED"},
+ {1237, L"ERROR_RETRY"},
+ {1238, L"ERROR_CONNECTION_COUNT_LIMIT"},
+ {1239, L"ERROR_LOGIN_TIME_RESTRICTION"},
+ {1240, L"ERROR_LOGIN_WKSTA_RESTRICTION"},
+ {1241, L"ERROR_INCORRECT_ADDRESS"},
+ {1242, L"ERROR_ALREADY_REGISTERED"},
+ {1243, L"ERROR_SERVICE_NOT_FOUND"},
+ {1244, L"ERROR_NOT_AUTHENTICATED"},
+ {1245, L"ERROR_NOT_LOGGED_ON"},
+ {1246, L"ERROR_CONTINUE"},
+ {1247, L"ERROR_ALREADY_INITIALIZED"},
+ {1248, L"ERROR_NO_MORE_DEVICES"},
+ {1249, L"ERROR_NO_SUCH_SITE"},
+ {1250, L"ERROR_DOMAIN_CONTROLLER_EXISTS"},
+ {1251, L"ERROR_ONLY_IF_CONNECTED"},
+ {1252, L"ERROR_OVERRIDE_NOCHANGES"},
+ {1253, L"ERROR_BAD_USER_PROFILE"},
+ {1254, L"ERROR_NOT_SUPPORTED_ON_SBS"},
+ {1255, L"ERROR_SERVER_SHUTDOWN_IN_PROGRESS"},
+ {1256, L"ERROR_HOST_DOWN"},
+ {1257, L"ERROR_NON_ACCOUNT_SID"},
+ {1258, L"ERROR_NON_DOMAIN_SID"},
+ {1259, L"ERROR_APPHELP_BLOCK"},
+ {1260, L"ERROR_ACCESS_DISABLED_BY_POLICY"},
+ {1261, L"ERROR_REG_NAT_CONSUMPTION"},
+ {1262, L"ERROR_CSCSHARE_OFFLINE"},
+ {1263, L"ERROR_PKINIT_FAILURE"},
+ {1264, L"ERROR_SMARTCARD_SUBSYSTEM_FAILURE"},
+ {1265, L"ERROR_DOWNGRADE_DETECTED"},
+ {1271, L"ERROR_MACHINE_LOCKED"},
+ {1273, L"ERROR_CALLBACK_SUPPLIED_INVALID_DATA"},
+ {1274, L"ERROR_SYNC_FOREGROUND_REFRESH_REQUIRED"},
+ {1275, L"ERROR_DRIVER_BLOCKED"},
+ {1276, L"ERROR_INVALID_IMPORT_OF_NON_DLL"},
+ {1277, L"ERROR_ACCESS_DISABLED_WEBBLADE"},
+ {1278, L"ERROR_ACCESS_DISABLED_WEBBLADE_TAMPER"},
+ {1279, L"ERROR_RECOVERY_FAILURE"},
+ {1280, L"ERROR_ALREADY_FIBER"},
+ {1281, L"ERROR_ALREADY_THREAD"},
+ {1282, L"ERROR_STACK_BUFFER_OVERRUN"},
+ {1283, L"ERROR_PARAMETER_QUOTA_EXCEEDED"},
+ {1284, L"ERROR_DEBUGGER_INACTIVE"},
+ {1285, L"ERROR_DELAY_LOAD_FAILED"},
+ {1286, L"ERROR_VDM_DISALLOWED"},
+ {1287, L"ERROR_UNIDENTIFIED_ERROR"},
+ {1288, L"ERROR_INVALID_CRUNTIME_PARAMETER"},
+ {1289, L"ERROR_BEYOND_VDL"},
+ {1290, L"ERROR_INCOMPATIBLE_SERVICE_SID_TYPE"},
+ {1291, L"ERROR_DRIVER_PROCESS_TERMINATED"},
+ {1292, L"ERROR_IMPLEMENTATION_LIMIT"},
+ {1293, L"ERROR_PROCESS_IS_PROTECTED"},
+ {1294, L"ERROR_SERVICE_NOTIFY_CLIENT_LAGGING"},
+ {1295, L"ERROR_DISK_QUOTA_EXCEEDED"},
+ {1296, L"ERROR_CONTENT_BLOCKED"},
+ {1297, L"ERROR_INCOMPATIBLE_SERVICE_PRIVILEGE"},
+ {1298, L"ERROR_APP_HANG"},
+ {1299, L"ERROR_INVALID_LABEL"},
+ {1300, L"ERROR_NOT_ALL_ASSIGNED"},
+ {1301, L"ERROR_SOME_NOT_MAPPED"},
+ {1302, L"ERROR_NO_QUOTAS_FOR_ACCOUNT"},
+ {1303, L"ERROR_LOCAL_USER_SESSION_KEY"},
+ {1304, L"ERROR_NULL_LM_PASSWORD"},
+ {1305, L"ERROR_UNKNOWN_REVISION"},
+ {1306, L"ERROR_REVISION_MISMATCH"},
+ {1307, L"ERROR_INVALID_OWNER"},
+ {1308, L"ERROR_INVALID_PRIMARY_GROUP"},
+ {1309, L"ERROR_NO_IMPERSONATION_TOKEN"},
+ {1310, L"ERROR_CANT_DISABLE_MANDATORY"},
+ {1311, L"ERROR_NO_LOGON_SERVERS"},
+ {1312, L"ERROR_NO_SUCH_LOGON_SESSION"},
+ {1313, L"ERROR_NO_SUCH_PRIVILEGE"},
+ {1314, L"ERROR_PRIVILEGE_NOT_HELD"},
+ {1315, L"ERROR_INVALID_ACCOUNT_NAME"},
+ {1316, L"ERROR_USER_EXISTS"},
+ {1317, L"ERROR_NO_SUCH_USER"},
+ {1318, L"ERROR_GROUP_EXISTS"},
+ {1319, L"ERROR_NO_SUCH_GROUP"},
+ {1320, L"ERROR_MEMBER_IN_GROUP"},
+ {1321, L"ERROR_MEMBER_NOT_IN_GROUP"},
+ {1322, L"ERROR_LAST_ADMIN"},
+ {1323, L"ERROR_WRONG_PASSWORD"},
+ {1324, L"ERROR_ILL_FORMED_PASSWORD"},
+ {1325, L"ERROR_PASSWORD_RESTRICTION"},
+ {1326, L"ERROR_LOGON_FAILURE"},
+ {1327, L"ERROR_ACCOUNT_RESTRICTION"},
+ {1328, L"ERROR_INVALID_LOGON_HOURS"},
+ {1329, L"ERROR_INVALID_WORKSTATION"},
+ {1330, L"ERROR_PASSWORD_EXPIRED"},
+ {1331, L"ERROR_ACCOUNT_DISABLED"},
+ {1332, L"ERROR_NONE_MAPPED"},
+ {1333, L"ERROR_TOO_MANY_LUIDS_REQUESTED"},
+ {1334, L"ERROR_LUIDS_EXHAUSTED"},
+ {1335, L"ERROR_INVALID_SUB_AUTHORITY"},
+ {1336, L"ERROR_INVALID_ACL"},
+ {1337, L"ERROR_INVALID_SID"},
+ {1338, L"ERROR_INVALID_SECURITY_DESCR"},
+ {1340, L"ERROR_BAD_INHERITANCE_ACL"},
+ {1341, L"ERROR_SERVER_DISABLED"},
+ {1342, L"ERROR_SERVER_NOT_DISABLED"},
+ {1343, L"ERROR_INVALID_ID_AUTHORITY"},
+ {1344, L"ERROR_ALLOTTED_SPACE_EXCEEDED"},
+ {1345, L"ERROR_INVALID_GROUP_ATTRIBUTES"},
+ {1346, L"ERROR_BAD_IMPERSONATION_LEVEL"},
+ {1347, L"ERROR_CANT_OPEN_ANONYMOUS"},
+ {1348, L"ERROR_BAD_VALIDATION_CLASS"},
+ {1349, L"ERROR_BAD_TOKEN_TYPE"},
+ {1350, L"ERROR_NO_SECURITY_ON_OBJECT"},
+ {1351, L"ERROR_CANT_ACCESS_DOMAIN_INFO"},
+ {1352, L"ERROR_INVALID_SERVER_STATE"},
+ {1353, L"ERROR_INVALID_DOMAIN_STATE"},
+ {1354, L"ERROR_INVALID_DOMAIN_ROLE"},
+ {1355, L"ERROR_NO_SUCH_DOMAIN"},
+ {1356, L"ERROR_DOMAIN_EXISTS"},
+ {1357, L"ERROR_DOMAIN_LIMIT_EXCEEDED"},
+ {1358, L"ERROR_INTERNAL_DB_CORRUPTION"},
+ {1359, L"ERROR_INTERNAL_ERROR"},
+ {1360, L"ERROR_GENERIC_NOT_MAPPED"},
+ {1361, L"ERROR_BAD_DESCRIPTOR_FORMAT"},
+ {1362, L"ERROR_NOT_LOGON_PROCESS"},
+ {1363, L"ERROR_LOGON_SESSION_EXISTS"},
+ {1364, L"ERROR_NO_SUCH_PACKAGE"},
+ {1365, L"ERROR_BAD_LOGON_SESSION_STATE"},
+ {1366, L"ERROR_LOGON_SESSION_COLLISION"},
+ {1367, L"ERROR_INVALID_LOGON_TYPE"},
+ {1368, L"ERROR_CANNOT_IMPERSONATE"},
+ {1369, L"ERROR_RXACT_INVALID_STATE"},
+ {1370, L"ERROR_RXACT_COMMIT_FAILURE"},
+ {1371, L"ERROR_SPECIAL_ACCOUNT"},
+ {1372, L"ERROR_SPECIAL_GROUP"},
+ {1373, L"ERROR_SPECIAL_USER"},
+ {1374, L"ERROR_MEMBERS_PRIMARY_GROUP"},
+ {1375, L"ERROR_TOKEN_ALREADY_IN_USE"},
+ {1376, L"ERROR_NO_SUCH_ALIAS"},
+ {1377, L"ERROR_MEMBER_NOT_IN_ALIAS"},
+ {1378, L"ERROR_MEMBER_IN_ALIAS"},
+ {1379, L"ERROR_ALIAS_EXISTS"},
+ {1380, L"ERROR_LOGON_NOT_GRANTED"},
+ {1381, L"ERROR_TOO_MANY_SECRETS"},
+ {1382, L"ERROR_SECRET_TOO_LONG"},
+ {1383, L"ERROR_INTERNAL_DB_ERROR"},
+ {1384, L"ERROR_TOO_MANY_CONTEXT_IDS"},
+ {1385, L"ERROR_LOGON_TYPE_NOT_GRANTED"},
+ {1386, L"ERROR_NT_CROSS_ENCRYPTION_REQUIRED"},
+ {1387, L"ERROR_NO_SUCH_MEMBER"},
+ {1388, L"ERROR_INVALID_MEMBER"},
+ {1389, L"ERROR_TOO_MANY_SIDS"},
+ {1390, L"ERROR_LM_CROSS_ENCRYPTION_REQUIRED"},
+ {1391, L"ERROR_NO_INHERITANCE"},
+ {1392, L"ERROR_FILE_CORRUPT"},
+ {1393, L"ERROR_DISK_CORRUPT"},
+ {1394, L"ERROR_NO_USER_SESSION_KEY"},
+ {1395, L"ERROR_LICENSE_QUOTA_EXCEEDED"},
+ {1396, L"ERROR_WRONG_TARGET_NAME"},
+ {1397, L"ERROR_MUTUAL_AUTH_FAILED"},
+ {1398, L"ERROR_TIME_SKEW"},
+ {1399, L"ERROR_CURRENT_DOMAIN_NOT_ALLOWED"},
+ {1400, L"ERROR_INVALID_WINDOW_HANDLE"},
+ {1401, L"ERROR_INVALID_MENU_HANDLE"},
+ {1402, L"ERROR_INVALID_CURSOR_HANDLE"},
+ {1403, L"ERROR_INVALID_ACCEL_HANDLE"},
+ {1404, L"ERROR_INVALID_HOOK_HANDLE"},
+ {1405, L"ERROR_INVALID_DWP_HANDLE"},
+ {1406, L"ERROR_TLW_WITH_WSCHILD"},
+ {1407, L"ERROR_CANNOT_FIND_WND_CLASS"},
+ {1408, L"ERROR_WINDOW_OF_OTHER_THREAD"},
+ {1409, L"ERROR_HOTKEY_ALREADY_REGISTERED"},
+ {1410, L"ERROR_CLASS_ALREADY_EXISTS"},
+ {1411, L"ERROR_CLASS_DOES_NOT_EXIST"},
+ {1412, L"ERROR_CLASS_HAS_WINDOWS"},
+ {1413, L"ERROR_INVALID_INDEX"},
+ {1414, L"ERROR_INVALID_ICON_HANDLE"},
+ {1415, L"ERROR_PRIVATE_DIALOG_INDEX"},
+ {1416, L"ERROR_LISTBOX_ID_NOT_FOUND"},
+ {1417, L"ERROR_NO_WILDCARD_CHARACTERS"},
+ {1418, L"ERROR_CLIPBOARD_NOT_OPEN"},
+ {1419, L"ERROR_HOTKEY_NOT_REGISTERED"},
+ {1420, L"ERROR_WINDOW_NOT_DIALOG"},
+ {1421, L"ERROR_CONTROL_ID_NOT_FOUND"},
+ {1422, L"ERROR_INVALID_COMBOBOX_MESSAGE"},
+ {1423, L"ERROR_WINDOW_NOT_COMBOBOX"},
+ {1424, L"ERROR_INVALID_EDIT_HEIGHT"},
+ {1425, L"ERROR_DC_NOT_FOUND"},
+ {1426, L"ERROR_INVALID_HOOK_FILTER"},
+ {1427, L"ERROR_INVALID_FILTER_PROC"},
+ {1428, L"ERROR_HOOK_NEEDS_HMOD"},
+ {1429, L"ERROR_GLOBAL_ONLY_HOOK"},
+ {1430, L"ERROR_JOURNAL_HOOK_SET"},
+ {1431, L"ERROR_HOOK_NOT_INSTALLED"},
+ {1432, L"ERROR_INVALID_LB_MESSAGE"},
+ {1433, L"ERROR_SETCOUNT_ON_BAD_LB"},
+ {1434, L"ERROR_LB_WITHOUT_TABSTOPS"},
+ {1435, L"ERROR_DESTROY_OBJECT_OF_OTHER_THREAD"},
+ {1436, L"ERROR_CHILD_WINDOW_MENU"},
+ {1437, L"ERROR_NO_SYSTEM_MENU"},
+ {1438, L"ERROR_INVALID_MSGBOX_STYLE"},
+ {1439, L"ERROR_INVALID_SPI_VALUE"},
+ {1440, L"ERROR_SCREEN_ALREADY_LOCKED"},
+ {1441, L"ERROR_HWNDS_HAVE_DIFF_PARENT"},
+ {1442, L"ERROR_NOT_CHILD_WINDOW"},
+ {1443, L"ERROR_INVALID_GW_COMMAND"},
+ {1444, L"ERROR_INVALID_THREAD_ID"},
+ {1445, L"ERROR_NON_MDICHILD_WINDOW"},
+ {1446, L"ERROR_POPUP_ALREADY_ACTIVE"},
+ {1447, L"ERROR_NO_SCROLLBARS"},
+ {1448, L"ERROR_INVALID_SCROLLBAR_RANGE"},
+ {1449, L"ERROR_INVALID_SHOWWIN_COMMAND"},
+ {1450, L"ERROR_NO_SYSTEM_RESOURCES"},
+ {1451, L"ERROR_NONPAGED_SYSTEM_RESOURCES"},
+ {1452, L"ERROR_PAGED_SYSTEM_RESOURCES"},
+ {1453, L"ERROR_WORKING_SET_QUOTA"},
+ {1454, L"ERROR_PAGEFILE_QUOTA"},
+ {1455, L"ERROR_COMMITMENT_LIMIT"},
+ {1456, L"ERROR_MENU_ITEM_NOT_FOUND"},
+ {1457, L"ERROR_INVALID_KEYBOARD_HANDLE"},
+ {1458, L"ERROR_HOOK_TYPE_NOT_ALLOWED"},
+ {1459, L"ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION"},
+ {1460, L"ERROR_TIMEOUT"},
+ {1461, L"ERROR_INVALID_MONITOR_HANDLE"},
+ {1462, L"ERROR_INCORRECT_SIZE"},
+ {1463, L"ERROR_SYMLINK_CLASS_DISABLED"},
+ {1464, L"ERROR_SYMLINK_NOT_SUPPORTED"},
+ {1465, L"ERROR_XML_PARSE_ERROR"},
+ {1466, L"ERROR_XMLDSIG_ERROR"},
+ {1467, L"ERROR_RESTART_APPLICATION"},
+ {1468, L"ERROR_WRONG_COMPARTMENT"},
+ {1469, L"ERROR_AUTHIP_FAILURE"},
+ {1470, L"ERROR_NO_NVRAM_RESOURCES"},
+ {1471, L"ERROR_NOT_GUI_PROCESS"},
+ {1500, L"ERROR_EVENTLOG_FILE_CORRUPT"},
+ {1501, L"ERROR_EVENTLOG_CANT_START"},
+ {1502, L"ERROR_LOG_FILE_FULL"},
+ {1503, L"ERROR_EVENTLOG_FILE_CHANGED"},
+ {1550, L"ERROR_INVALID_TASK_NAME"},
+ {1551, L"ERROR_INVALID_TASK_INDEX"},
+ {1552, L"ERROR_THREAD_ALREADY_IN_TASK"},
+ {1601, L"ERROR_INSTALL_SERVICE_FAILURE"},
+ {1602, L"ERROR_INSTALL_USEREXIT"},
+ {1603, L"ERROR_INSTALL_FAILURE"},
+ {1604, L"ERROR_INSTALL_SUSPEND"},
+ {1605, L"ERROR_UNKNOWN_PRODUCT"},
+ {1606, L"ERROR_UNKNOWN_FEATURE"},
+ {1607, L"ERROR_UNKNOWN_COMPONENT"},
+ {1608, L"ERROR_UNKNOWN_PROPERTY"},
+ {1609, L"ERROR_INVALID_HANDLE_STATE"},
+ {1610, L"ERROR_BAD_CONFIGURATION"},
+ {1611, L"ERROR_INDEX_ABSENT"},
+ {1612, L"ERROR_INSTALL_SOURCE_ABSENT"},
+ {1613, L"ERROR_INSTALL_PACKAGE_VERSION"},
+ {1614, L"ERROR_PRODUCT_UNINSTALLED"},
+ {1615, L"ERROR_BAD_QUERY_SYNTAX"},
+ {1616, L"ERROR_INVALID_FIELD"},
+ {1617, L"ERROR_DEVICE_REMOVED"},
+ {1618, L"ERROR_INSTALL_ALREADY_RUNNING"},
+ {1619, L"ERROR_INSTALL_PACKAGE_OPEN_FAILED"},
+ {1620, L"ERROR_INSTALL_PACKAGE_INVALID"},
+ {1621, L"ERROR_INSTALL_UI_FAILURE"},
+ {1622, L"ERROR_INSTALL_LOG_FAILURE"},
+ {1623, L"ERROR_INSTALL_LANGUAGE_UNSUPPORTED"},
+ {1624, L"ERROR_INSTALL_TRANSFORM_FAILURE"},
+ {1625, L"ERROR_INSTALL_PACKAGE_REJECTED"},
+ {1626, L"ERROR_FUNCTION_NOT_CALLED"},
+ {1627, L"ERROR_FUNCTION_FAILED"},
+ {1628, L"ERROR_INVALID_TABLE"},
+ {1629, L"ERROR_DATATYPE_MISMATCH"},
+ {1630, L"ERROR_UNSUPPORTED_TYPE"},
+ {1631, L"ERROR_CREATE_FAILED"},
+ {1632, L"ERROR_INSTALL_TEMP_UNWRITABLE"},
+ {1633, L"ERROR_INSTALL_PLATFORM_UNSUPPORTED"},
+ {1634, L"ERROR_INSTALL_NOTUSED"},
+ {1635, L"ERROR_PATCH_PACKAGE_OPEN_FAILED"},
+ {1636, L"ERROR_PATCH_PACKAGE_INVALID"},
+ {1637, L"ERROR_PATCH_PACKAGE_UNSUPPORTED"},
+ {1638, L"ERROR_PRODUCT_VERSION"},
+ {1639, L"ERROR_INVALID_COMMAND_LINE"},
+ {1640, L"ERROR_INSTALL_REMOTE_DISALLOWED"},
+ {1641, L"ERROR_SUCCESS_REBOOT_INITIATED"},
+ {1642, L"ERROR_PATCH_TARGET_NOT_FOUND"},
+ {1643, L"ERROR_PATCH_PACKAGE_REJECTED"},
+ {1644, L"ERROR_INSTALL_TRANSFORM_REJECTED"},
+ {1645, L"ERROR_INSTALL_REMOTE_PROHIBITED"},
+ {1646, L"ERROR_PATCH_REMOVAL_UNSUPPORTED"},
+ {1647, L"ERROR_UNKNOWN_PATCH"},
+ {1648, L"ERROR_PATCH_NO_SEQUENCE"},
+ {1649, L"ERROR_PATCH_REMOVAL_DISALLOWED"},
+ {1650, L"ERROR_INVALID_PATCH_XML"},
+ {1651, L"ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT"},
+ {1652, L"ERROR_INSTALL_SERVICE_SAFEBOOT"},
+ {1653, L"ERROR_FAIL_FAST_EXCEPTION"},
+ {1654, L"ERROR_INSTALL_REJECTED"},
+ {1700, L"RPC_S_INVALID_STRING_BINDING"},
+ {1701, L"RPC_S_WRONG_KIND_OF_BINDING"},
+ {1702, L"RPC_S_INVALID_BINDING"},
+ {1703, L"RPC_S_PROTSEQ_NOT_SUPPORTED"},
+ {1704, L"RPC_S_INVALID_RPC_PROTSEQ"},
+ {1705, L"RPC_S_INVALID_STRING_UUID"},
+ {1706, L"RPC_S_INVALID_ENDPOINT_FORMAT"},
+ {1707, L"RPC_S_INVALID_NET_ADDR"},
+ {1708, L"RPC_S_NO_ENDPOINT_FOUND"},
+ {1709, L"RPC_S_INVALID_TIMEOUT"},
+ {1710, L"RPC_S_OBJECT_NOT_FOUND"},
+ {1711, L"RPC_S_ALREADY_REGISTERED"},
+ {1712, L"RPC_S_TYPE_ALREADY_REGISTERED"},
+ {1713, L"RPC_S_ALREADY_LISTENING"},
+ {1714, L"RPC_S_NO_PROTSEQS_REGISTERED"},
+ {1715, L"RPC_S_NOT_LISTENING"},
+ {1716, L"RPC_S_UNKNOWN_MGR_TYPE"},
+ {1717, L"RPC_S_UNKNOWN_IF"},
+ {1718, L"RPC_S_NO_BINDINGS"},
+ {1719, L"RPC_S_NO_PROTSEQS"},
+ {1720, L"RPC_S_CANT_CREATE_ENDPOINT"},
+ {1721, L"RPC_S_OUT_OF_RESOURCES"},
+ {1722, L"RPC_S_SERVER_UNAVAILABLE"},
+ {1723, L"RPC_S_SERVER_TOO_BUSY"},
+ {1724, L"RPC_S_INVALID_NETWORK_OPTIONS"},
+ {1725, L"RPC_S_NO_CALL_ACTIVE"},
+ {1726, L"RPC_S_CALL_FAILED"},
+ {1727, L"RPC_S_CALL_FAILED_DNE"},
+ {1728, L"RPC_S_PROTOCOL_ERROR"},
+ {1729, L"RPC_S_PROXY_ACCESS_DENIED"},
+ {1730, L"RPC_S_UNSUPPORTED_TRANS_SYN"},
+ {1732, L"RPC_S_UNSUPPORTED_TYPE"},
+ {1733, L"RPC_S_INVALID_TAG"},
+ {1734, L"RPC_S_INVALID_BOUND"},
+ {1735, L"RPC_S_NO_ENTRY_NAME"},
+ {1736, L"RPC_S_INVALID_NAME_SYNTAX"},
+ {1737, L"RPC_S_UNSUPPORTED_NAME_SYNTAX"},
+ {1739, L"RPC_S_UUID_NO_ADDRESS"},
+ {1740, L"RPC_S_DUPLICATE_ENDPOINT"},
+ {1741, L"RPC_S_UNKNOWN_AUTHN_TYPE"},
+ {1742, L"RPC_S_MAX_CALLS_TOO_SMALL"},
+ {1743, L"RPC_S_STRING_TOO_LONG"},
+ {1744, L"RPC_S_PROTSEQ_NOT_FOUND"},
+ {1745, L"RPC_S_PROCNUM_OUT_OF_RANGE"},
+ {1746, L"RPC_S_BINDING_HAS_NO_AUTH"},
+ {1747, L"RPC_S_UNKNOWN_AUTHN_SERVICE"},
+ {1748, L"RPC_S_UNKNOWN_AUTHN_LEVEL"},
+ {1749, L"RPC_S_INVALID_AUTH_IDENTITY"},
+ {1750, L"RPC_S_UNKNOWN_AUTHZ_SERVICE"},
+ {1751, L"EPT_S_INVALID_ENTRY"},
+ {1752, L"EPT_S_CANT_PERFORM_OP"},
+ {1753, L"EPT_S_NOT_REGISTERED"},
+ {1754, L"RPC_S_NOTHING_TO_EXPORT"},
+ {1755, L"RPC_S_INCOMPLETE_NAME"},
+ {1756, L"RPC_S_INVALID_VERS_OPTION"},
+ {1757, L"RPC_S_NO_MORE_MEMBERS"},
+ {1758, L"RPC_S_NOT_ALL_OBJS_UNEXPORTED"},
+ {1759, L"RPC_S_INTERFACE_NOT_FOUND"},
+ {1760, L"RPC_S_ENTRY_ALREADY_EXISTS"},
+ {1761, L"RPC_S_ENTRY_NOT_FOUND"},
+ {1762, L"RPC_S_NAME_SERVICE_UNAVAILABLE"},
+ {1763, L"RPC_S_INVALID_NAF_ID"},
+ {1764, L"RPC_S_CANNOT_SUPPORT"},
+ {1765, L"RPC_S_NO_CONTEXT_AVAILABLE"},
+ {1766, L"RPC_S_INTERNAL_ERROR"},
+ {1767, L"RPC_S_ZERO_DIVIDE"},
+ {1768, L"RPC_S_ADDRESS_ERROR"},
+ {1769, L"RPC_S_FP_DIV_ZERO"},
+ {1770, L"RPC_S_FP_UNDERFLOW"},
+ {1771, L"RPC_S_FP_OVERFLOW"},
+ {1772, L"RPC_X_NO_MORE_ENTRIES"},
+ {1773, L"RPC_X_SS_CHAR_TRANS_OPEN_FAIL"},
+ {1774, L"RPC_X_SS_CHAR_TRANS_SHORT_FILE"},
+ {1775, L"RPC_X_SS_IN_NULL_CONTEXT"},
+ {1777, L"RPC_X_SS_CONTEXT_DAMAGED"},
+ {1778, L"RPC_X_SS_HANDLES_MISMATCH"},
+ {1779, L"RPC_X_SS_CANNOT_GET_CALL_HANDLE"},
+ {1780, L"RPC_X_NULL_REF_POINTER"},
+ {1781, L"RPC_X_ENUM_VALUE_OUT_OF_RANGE"},
+ {1782, L"RPC_X_BYTE_COUNT_TOO_SMALL"},
+ {1783, L"RPC_X_BAD_STUB_DATA"},
+ {1784, L"ERROR_INVALID_USER_BUFFER"},
+ {1785, L"ERROR_UNRECOGNIZED_MEDIA"},
+ {1786, L"ERROR_NO_TRUST_LSA_SECRET"},
+ {1787, L"ERROR_NO_TRUST_SAM_ACCOUNT"},
+ {1788, L"ERROR_TRUSTED_DOMAIN_FAILURE"},
+ {1789, L"ERROR_TRUSTED_RELATIONSHIP_FAILURE"},
+ {1790, L"ERROR_TRUST_FAILURE"},
+ {1791, L"RPC_S_CALL_IN_PROGRESS"},
+ {1792, L"ERROR_NETLOGON_NOT_STARTED"},
+ {1793, L"ERROR_ACCOUNT_EXPIRED"},
+ {1794, L"ERROR_REDIRECTOR_HAS_OPEN_HANDLES"},
+ {1795, L"ERROR_PRINTER_DRIVER_ALREADY_INSTALLED"},
+ {1796, L"ERROR_UNKNOWN_PORT"},
+ {1797, L"ERROR_UNKNOWN_PRINTER_DRIVER"},
+ {1798, L"ERROR_UNKNOWN_PRINTPROCESSOR"},
+ {1799, L"ERROR_INVALID_SEPARATOR_FILE"},
+ {1800, L"ERROR_INVALID_PRIORITY"},
+ {1801, L"ERROR_INVALID_PRINTER_NAME"},
+ {1802, L"ERROR_PRINTER_ALREADY_EXISTS"},
+ {1803, L"ERROR_INVALID_PRINTER_COMMAND"},
+ {1804, L"ERROR_INVALID_DATATYPE"},
+ {1805, L"ERROR_INVALID_ENVIRONMENT"},
+ {1806, L"RPC_S_NO_MORE_BINDINGS"},
+ {1807, L"ERROR_NOLOGON_INTERDOMAIN_TRUST_ACCOUNT"},
+ {1808, L"ERROR_NOLOGON_WORKSTATION_TRUST_ACCOUNT"},
+ {1809, L"ERROR_NOLOGON_SERVER_TRUST_ACCOUNT"},
+ {1810, L"ERROR_DOMAIN_TRUST_INCONSISTENT"},
+ {1811, L"ERROR_SERVER_HAS_OPEN_HANDLES"},
+ {1812, L"ERROR_RESOURCE_DATA_NOT_FOUND"},
+ {1813, L"ERROR_RESOURCE_TYPE_NOT_FOUND"},
+ {1814, L"ERROR_RESOURCE_NAME_NOT_FOUND"},
+ {1815, L"ERROR_RESOURCE_LANG_NOT_FOUND"},
+ {1816, L"ERROR_NOT_ENOUGH_QUOTA"},
+ {1817, L"RPC_S_NO_INTERFACES"},
+ {1818, L"RPC_S_CALL_CANCELLED"},
+ {1819, L"RPC_S_BINDING_INCOMPLETE"},
+ {1820, L"RPC_S_COMM_FAILURE"},
+ {1821, L"RPC_S_UNSUPPORTED_AUTHN_LEVEL"},
+ {1822, L"RPC_S_NO_PRINC_NAME"},
+ {1823, L"RPC_S_NOT_RPC_ERROR"},
+ {1824, L"RPC_S_UUID_LOCAL_ONLY"},
+ {1825, L"RPC_S_SEC_PKG_ERROR"},
+ {1826, L"RPC_S_NOT_CANCELLED"},
+ {1827, L"RPC_X_INVALID_ES_ACTION"},
+ {1828, L"RPC_X_WRONG_ES_VERSION"},
+ {1829, L"RPC_X_WRONG_STUB_VERSION"},
+ {1830, L"RPC_X_INVALID_PIPE_OBJECT"},
+ {1831, L"RPC_X_WRONG_PIPE_ORDER"},
+ {1832, L"RPC_X_WRONG_PIPE_VERSION"},
+ {1833, L"RPC_S_COOKIE_AUTH_FAILED"},
+ {1898, L"RPC_S_GROUP_MEMBER_NOT_FOUND"},
+ {1899, L"EPT_S_CANT_CREATE"},
+ {1900, L"RPC_S_INVALID_OBJECT"},
+ {1901, L"ERROR_INVALID_TIME"},
+ {1902, L"ERROR_INVALID_FORM_NAME"},
+ {1903, L"ERROR_INVALID_FORM_SIZE"},
+ {1904, L"ERROR_ALREADY_WAITING"},
+ {1905, L"ERROR_PRINTER_DELETED"},
+ {1906, L"ERROR_INVALID_PRINTER_STATE"},
+ {1907, L"ERROR_PASSWORD_MUST_CHANGE"},
+ {1908, L"ERROR_DOMAIN_CONTROLLER_NOT_FOUND"},
+ {1909, L"ERROR_ACCOUNT_LOCKED_OUT"},
+ {1910, L"OR_INVALID_OXID"},
+ {1911, L"OR_INVALID_OID"},
+ {1912, L"OR_INVALID_SET"},
+ {1913, L"RPC_S_SEND_INCOMPLETE"},
+ {1914, L"RPC_S_INVALID_ASYNC_HANDLE"},
+ {1915, L"RPC_S_INVALID_ASYNC_CALL"},
+ {1916, L"RPC_X_PIPE_CLOSED"},
+ {1917, L"RPC_X_PIPE_DISCIPLINE_ERROR"},
+ {1918, L"RPC_X_PIPE_EMPTY"},
+ {1919, L"ERROR_NO_SITENAME"},
+ {1920, L"ERROR_CANT_ACCESS_FILE"},
+ {1921, L"ERROR_CANT_RESOLVE_FILENAME"},
+ {1922, L"RPC_S_ENTRY_TYPE_MISMATCH"},
+ {1923, L"RPC_S_NOT_ALL_OBJS_EXPORTED"},
+ {1924, L"RPC_S_INTERFACE_NOT_EXPORTED"},
+ {1925, L"RPC_S_PROFILE_NOT_ADDED"},
+ {1926, L"RPC_S_PRF_ELT_NOT_ADDED"},
+ {1927, L"RPC_S_PRF_ELT_NOT_REMOVED"},
+ {1928, L"RPC_S_GRP_ELT_NOT_ADDED"},
+ {1929, L"RPC_S_GRP_ELT_NOT_REMOVED"},
+ {1930, L"ERROR_KM_DRIVER_BLOCKED"},
+ {1931, L"ERROR_CONTEXT_EXPIRED"},
+ {1932, L"ERROR_PER_USER_TRUST_QUOTA_EXCEEDED"},
+ {1933, L"ERROR_ALL_USER_TRUST_QUOTA_EXCEEDED"},
+ {1934, L"ERROR_USER_DELETE_TRUST_QUOTA_EXCEEDED"},
+ {1935, L"ERROR_AUTHENTICATION_FIREWALL_FAILED"},
+ {1936, L"ERROR_REMOTE_PRINT_CONNECTIONS_BLOCKED"},
+ {1937, L"ERROR_NTLM_BLOCKED"},
+ {1938, L"ERROR_PASSWORD_CHANGE_REQUIRED"},
+ {2000, L"ERROR_INVALID_PIXEL_FORMAT"},
+ {2001, L"ERROR_BAD_DRIVER"},
+ {2002, L"ERROR_INVALID_WINDOW_STYLE"},
+ {2003, L"ERROR_METAFILE_NOT_SUPPORTED"},
+ {2004, L"ERROR_TRANSFORM_NOT_SUPPORTED"},
+ {2005, L"ERROR_CLIPPING_NOT_SUPPORTED"},
+ {2010, L"ERROR_INVALID_CMM"},
+ {2011, L"ERROR_INVALID_PROFILE"},
+ {2012, L"ERROR_TAG_NOT_FOUND"},
+ {2013, L"ERROR_TAG_NOT_PRESENT"},
+ {2014, L"ERROR_DUPLICATE_TAG"},
+ {2015, L"ERROR_PROFILE_NOT_ASSOCIATED_WITH_DEVICE"},
+ {2016, L"ERROR_PROFILE_NOT_FOUND"},
+ {2017, L"ERROR_INVALID_COLORSPACE"},
+ {2018, L"ERROR_ICM_NOT_ENABLED"},
+ {2019, L"ERROR_DELETING_ICM_XFORM"},
+ {2020, L"ERROR_INVALID_TRANSFORM"},
+ {2021, L"ERROR_COLORSPACE_MISMATCH"},
+ {2022, L"ERROR_INVALID_COLORINDEX"},
+ {2023, L"ERROR_PROFILE_DOES_NOT_MATCH_DEVICE"},
+ {2108, L"ERROR_CONNECTED_OTHER_PASSWORD"},
+ {2109, L"ERROR_CONNECTED_OTHER_PASSWORD_DEFAULT"},
+ {2202, L"ERROR_BAD_USERNAME"},
+ {2250, L"ERROR_NOT_CONNECTED"},
+ {2401, L"ERROR_OPEN_FILES"},
+ {2402, L"ERROR_ACTIVE_CONNECTIONS"},
+ {2404, L"ERROR_DEVICE_IN_USE"},
+ {3000, L"ERROR_UNKNOWN_PRINT_MONITOR"},
+ {3001, L"ERROR_PRINTER_DRIVER_IN_USE"},
+ {3002, L"ERROR_SPOOL_FILE_NOT_FOUND"},
+ {3003, L"ERROR_SPL_NO_STARTDOC"},
+ {3004, L"ERROR_SPL_NO_ADDJOB"},
+ {3005, L"ERROR_PRINT_PROCESSOR_ALREADY_INSTALLED"},
+ {3006, L"ERROR_PRINT_MONITOR_ALREADY_INSTALLED"},
+ {3007, L"ERROR_INVALID_PRINT_MONITOR"},
+ {3008, L"ERROR_PRINT_MONITOR_IN_USE"},
+ {3009, L"ERROR_PRINTER_HAS_JOBS_QUEUED"},
+ {3010, L"ERROR_SUCCESS_REBOOT_REQUIRED"},
+ {3011, L"ERROR_SUCCESS_RESTART_REQUIRED"},
+ {3012, L"ERROR_PRINTER_NOT_FOUND"},
+ {3013, L"ERROR_PRINTER_DRIVER_WARNED"},
+ {3014, L"ERROR_PRINTER_DRIVER_BLOCKED"},
+ {3015, L"ERROR_PRINTER_DRIVER_PACKAGE_IN_USE"},
+ {3016, L"ERROR_CORE_DRIVER_PACKAGE_NOT_FOUND"},
+ {3017, L"ERROR_FAIL_REBOOT_REQUIRED"},
+ {3018, L"ERROR_FAIL_REBOOT_INITIATED"},
+ {3019, L"ERROR_PRINTER_DRIVER_DOWNLOAD_NEEDED"},
+ {3020, L"ERROR_PRINT_JOB_RESTART_REQUIRED"},
+ {3021, L"ERROR_INVALID_PRINTER_DRIVER_MANIFEST"},
+ {3022, L"ERROR_PRINTER_NOT_SHAREABLE"},
+ {3050, L"ERROR_REQUEST_PAUSED"},
+ {3950, L"ERROR_IO_REISSUE_AS_CACHED"},
+ {4000, L"ERROR_WINS_INTERNAL"},
+ {4001, L"ERROR_CAN_NOT_DEL_LOCAL_WINS"},
+ {4002, L"ERROR_STATIC_INIT"},
+ {4003, L"ERROR_INC_BACKUP"},
+ {4004, L"ERROR_FULL_BACKUP"},
+ {4005, L"ERROR_REC_NON_EXISTENT"},
+ {4006, L"ERROR_RPL_NOT_ALLOWED"},
+ {4050, L"PEERDIST_ERROR_CONTENTINFO_VERSION_UNSUPPORTED"},
+ {4051, L"PEERDIST_ERROR_CANNOT_PARSE_CONTENTINFO"},
+ {4052, L"PEERDIST_ERROR_MISSING_DATA"},
+ {4053, L"PEERDIST_ERROR_NO_MORE"},
+ {4054, L"PEERDIST_ERROR_NOT_INITIALIZED"},
+ {4055, L"PEERDIST_ERROR_ALREADY_INITIALIZED"},
+ {4056, L"PEERDIST_ERROR_SHUTDOWN_IN_PROGRESS"},
+ {4057, L"PEERDIST_ERROR_INVALIDATED"},
+ {4058, L"PEERDIST_ERROR_ALREADY_EXISTS"},
+ {4059, L"PEERDIST_ERROR_OPERATION_NOTFOUND"},
+ {4060, L"PEERDIST_ERROR_ALREADY_COMPLETED"},
+ {4061, L"PEERDIST_ERROR_OUT_OF_BOUNDS"},
+ {4062, L"PEERDIST_ERROR_VERSION_UNSUPPORTED"},
+ {4063, L"PEERDIST_ERROR_INVALID_CONFIGURATION"},
+ {4064, L"PEERDIST_ERROR_NOT_LICENSED"},
+ {4065, L"PEERDIST_ERROR_SERVICE_UNAVAILABLE"},
+ {4066, L"PEERDIST_ERROR_TRUST_FAILURE"},
+ {4100, L"ERROR_DHCP_ADDRESS_CONFLICT"},
+ {4200, L"ERROR_WMI_GUID_NOT_FOUND"},
+ {4201, L"ERROR_WMI_INSTANCE_NOT_FOUND"},
+ {4202, L"ERROR_WMI_ITEMID_NOT_FOUND"},
+ {4203, L"ERROR_WMI_TRY_AGAIN"},
+ {4204, L"ERROR_WMI_DP_NOT_FOUND"},
+ {4205, L"ERROR_WMI_UNRESOLVED_INSTANCE_REF"},
+ {4206, L"ERROR_WMI_ALREADY_ENABLED"},
+ {4207, L"ERROR_WMI_GUID_DISCONNECTED"},
+ {4208, L"ERROR_WMI_SERVER_UNAVAILABLE"},
+ {4209, L"ERROR_WMI_DP_FAILED"},
+ {4210, L"ERROR_WMI_INVALID_MOF"},
+ {4211, L"ERROR_WMI_INVALID_REGINFO"},
+ {4212, L"ERROR_WMI_ALREADY_DISABLED"},
+ {4213, L"ERROR_WMI_READ_ONLY"},
+ {4214, L"ERROR_WMI_SET_FAILURE"},
+ {4250, L"ERROR_NOT_APPCONTAINER"},
+ {4251, L"ERROR_APPCONTAINER_REQUIRED"},
+ {4252, L"ERROR_NOT_SUPPORTED_IN_APPCONTAINER"},
+ {4253, L"ERROR_INVALID_PACKAGE_SID_LENGTH"},
+ {4300, L"ERROR_INVALID_MEDIA"},
+ {4301, L"ERROR_INVALID_LIBRARY"},
+ {4302, L"ERROR_INVALID_MEDIA_POOL"},
+ {4303, L"ERROR_DRIVE_MEDIA_MISMATCH"},
+ {4304, L"ERROR_MEDIA_OFFLINE"},
+ {4305, L"ERROR_LIBRARY_OFFLINE"},
+ {4306, L"ERROR_EMPTY"},
+ {4307, L"ERROR_NOT_EMPTY"},
+ {4308, L"ERROR_MEDIA_UNAVAILABLE"},
+ {4309, L"ERROR_RESOURCE_DISABLED"},
+ {4310, L"ERROR_INVALID_CLEANER"},
+ {4311, L"ERROR_UNABLE_TO_CLEAN"},
+ {4312, L"ERROR_OBJECT_NOT_FOUND"},
+ {4313, L"ERROR_DATABASE_FAILURE"},
+ {4314, L"ERROR_DATABASE_FULL"},
+ {4315, L"ERROR_MEDIA_INCOMPATIBLE"},
+ {4316, L"ERROR_RESOURCE_NOT_PRESENT"},
+ {4317, L"ERROR_INVALID_OPERATION"},
+ {4318, L"ERROR_MEDIA_NOT_AVAILABLE"},
+ {4319, L"ERROR_DEVICE_NOT_AVAILABLE"},
+ {4320, L"ERROR_REQUEST_REFUSED"},
+ {4321, L"ERROR_INVALID_DRIVE_OBJECT"},
+ {4322, L"ERROR_LIBRARY_FULL"},
+ {4323, L"ERROR_MEDIUM_NOT_ACCESSIBLE"},
+ {4324, L"ERROR_UNABLE_TO_LOAD_MEDIUM"},
+ {4325, L"ERROR_UNABLE_TO_INVENTORY_DRIVE"},
+ {4326, L"ERROR_UNABLE_TO_INVENTORY_SLOT"},
+ {4327, L"ERROR_UNABLE_TO_INVENTORY_TRANSPORT"},
+ {4328, L"ERROR_TRANSPORT_FULL"},
+ {4329, L"ERROR_CONTROLLING_IEPORT"},
+ {4330, L"ERROR_UNABLE_TO_EJECT_MOUNTED_MEDIA"},
+ {4331, L"ERROR_CLEANER_SLOT_SET"},
+ {4332, L"ERROR_CLEANER_SLOT_NOT_SET"},
+ {4333, L"ERROR_CLEANER_CARTRIDGE_SPENT"},
+ {4334, L"ERROR_UNEXPECTED_OMID"},
+ {4335, L"ERROR_CANT_DELETE_LAST_ITEM"},
+ {4336, L"ERROR_MESSAGE_EXCEEDS_MAX_SIZE"},
+ {4337, L"ERROR_VOLUME_CONTAINS_SYS_FILES"},
+ {4338, L"ERROR_INDIGENOUS_TYPE"},
+ {4339, L"ERROR_NO_SUPPORTING_DRIVES"},
+ {4340, L"ERROR_CLEANER_CARTRIDGE_INSTALLED"},
+ {4341, L"ERROR_IEPORT_FULL"},
+ {4350, L"ERROR_FILE_OFFLINE"},
+ {4351, L"ERROR_REMOTE_STORAGE_NOT_ACTIVE"},
+ {4352, L"ERROR_REMOTE_STORAGE_MEDIA_ERROR"},
+ {4390, L"ERROR_NOT_A_REPARSE_POINT"},
+ {4391, L"ERROR_REPARSE_ATTRIBUTE_CONFLICT"},
+ {4392, L"ERROR_INVALID_REPARSE_DATA"},
+ {4393, L"ERROR_REPARSE_TAG_INVALID"},
+ {4394, L"ERROR_REPARSE_TAG_MISMATCH"},
+ {4400, L"ERROR_APP_DATA_NOT_FOUND"},
+ {4401, L"ERROR_APP_DATA_EXPIRED"},
+ {4402, L"ERROR_APP_DATA_CORRUPT"},
+ {4403, L"ERROR_APP_DATA_LIMIT_EXCEEDED"},
+ {4404, L"ERROR_APP_DATA_REBOOT_REQUIRED"},
+ {4420, L"ERROR_SECUREBOOT_ROLLBACK_DETECTED"},
+ {4421, L"ERROR_SECUREBOOT_POLICY_VIOLATION"},
+ {4422, L"ERROR_SECUREBOOT_INVALID_POLICY"},
+ {4423, L"ERROR_SECUREBOOT_POLICY_PUBLISHER_NOT_FOUND"},
+ {4424, L"ERROR_SECUREBOOT_POLICY_NOT_SIGNED"},
+ {4425, L"ERROR_SECUREBOOT_NOT_ENABLED"},
+ {4426, L"ERROR_SECUREBOOT_FILE_REPLACED"},
+ {4440, L"ERROR_OFFLOAD_READ_FLT_NOT_SUPPORTED"},
+ {4441, L"ERROR_OFFLOAD_WRITE_FLT_NOT_SUPPORTED"},
+ {4442, L"ERROR_OFFLOAD_READ_FILE_NOT_SUPPORTED"},
+ {4443, L"ERROR_OFFLOAD_WRITE_FILE_NOT_SUPPORTED"},
+ {4500, L"ERROR_VOLUME_NOT_SIS_ENABLED"},
+ {5001, L"ERROR_DEPENDENT_RESOURCE_EXISTS"},
+ {5002, L"ERROR_DEPENDENCY_NOT_FOUND"},
+ {5003, L"ERROR_DEPENDENCY_ALREADY_EXISTS"},
+ {5004, L"ERROR_RESOURCE_NOT_ONLINE"},
+ {5005, L"ERROR_HOST_NODE_NOT_AVAILABLE"},
+ {5006, L"ERROR_RESOURCE_NOT_AVAILABLE"},
+ {5007, L"ERROR_RESOURCE_NOT_FOUND"},
+ {5008, L"ERROR_SHUTDOWN_CLUSTER"},
+ {5009, L"ERROR_CANT_EVICT_ACTIVE_NODE"},
+ {5010, L"ERROR_OBJECT_ALREADY_EXISTS"},
+ {5011, L"ERROR_OBJECT_IN_LIST"},
+ {5012, L"ERROR_GROUP_NOT_AVAILABLE"},
+ {5013, L"ERROR_GROUP_NOT_FOUND"},
+ {5014, L"ERROR_GROUP_NOT_ONLINE"},
+ {5015, L"ERROR_HOST_NODE_NOT_RESOURCE_OWNER"},
+ {5016, L"ERROR_HOST_NODE_NOT_GROUP_OWNER"},
+ {5017, L"ERROR_RESMON_CREATE_FAILED"},
+ {5018, L"ERROR_RESMON_ONLINE_FAILED"},
+ {5019, L"ERROR_RESOURCE_ONLINE"},
+ {5020, L"ERROR_QUORUM_RESOURCE"},
+ {5021, L"ERROR_NOT_QUORUM_CAPABLE"},
+ {5022, L"ERROR_CLUSTER_SHUTTING_DOWN"},
+ {5023, L"ERROR_INVALID_STATE"},
+ {5024, L"ERROR_RESOURCE_PROPERTIES_STORED"},
+ {5025, L"ERROR_NOT_QUORUM_CLASS"},
+ {5026, L"ERROR_CORE_RESOURCE"},
+ {5027, L"ERROR_QUORUM_RESOURCE_ONLINE_FAILED"},
+ {5028, L"ERROR_QUORUMLOG_OPEN_FAILED"},
+ {5029, L"ERROR_CLUSTERLOG_CORRUPT"},
+ {5030, L"ERROR_CLUSTERLOG_RECORD_EXCEEDS_MAXSIZE"},
+ {5031, L"ERROR_CLUSTERLOG_EXCEEDS_MAXSIZE"},
+ {5032, L"ERROR_CLUSTERLOG_CHKPOINT_NOT_FOUND"},
+ {5033, L"ERROR_CLUSTERLOG_NOT_ENOUGH_SPACE"},
+ {5034, L"ERROR_QUORUM_OWNER_ALIVE"},
+ {5035, L"ERROR_NETWORK_NOT_AVAILABLE"},
+ {5036, L"ERROR_NODE_NOT_AVAILABLE"},
+ {5037, L"ERROR_ALL_NODES_NOT_AVAILABLE"},
+ {5038, L"ERROR_RESOURCE_FAILED"},
+ {5039, L"ERROR_CLUSTER_INVALID_NODE"},
+ {5040, L"ERROR_CLUSTER_NODE_EXISTS"},
+ {5041, L"ERROR_CLUSTER_JOIN_IN_PROGRESS"},
+ {5042, L"ERROR_CLUSTER_NODE_NOT_FOUND"},
+ {5043, L"ERROR_CLUSTER_LOCAL_NODE_NOT_FOUND"},
+ {5044, L"ERROR_CLUSTER_NETWORK_EXISTS"},
+ {5045, L"ERROR_CLUSTER_NETWORK_NOT_FOUND"},
+ {5046, L"ERROR_CLUSTER_NETINTERFACE_EXISTS"},
+ {5047, L"ERROR_CLUSTER_NETINTERFACE_NOT_FOUND"},
+ {5048, L"ERROR_CLUSTER_INVALID_REQUEST"},
+ {5049, L"ERROR_CLUSTER_INVALID_NETWORK_PROVIDER"},
+ {5050, L"ERROR_CLUSTER_NODE_DOWN"},
+ {5051, L"ERROR_CLUSTER_NODE_UNREACHABLE"},
+ {5052, L"ERROR_CLUSTER_NODE_NOT_MEMBER"},
+ {5053, L"ERROR_CLUSTER_JOIN_NOT_IN_PROGRESS"},
+ {5054, L"ERROR_CLUSTER_INVALID_NETWORK"},
+ {5056, L"ERROR_CLUSTER_NODE_UP"},
+ {5057, L"ERROR_CLUSTER_IPADDR_IN_USE"},
+ {5058, L"ERROR_CLUSTER_NODE_NOT_PAUSED"},
+ {5059, L"ERROR_CLUSTER_NO_SECURITY_CONTEXT"},
+ {5060, L"ERROR_CLUSTER_NETWORK_NOT_INTERNAL"},
+ {5061, L"ERROR_CLUSTER_NODE_ALREADY_UP"},
+ {5062, L"ERROR_CLUSTER_NODE_ALREADY_DOWN"},
+ {5063, L"ERROR_CLUSTER_NETWORK_ALREADY_ONLINE"},
+ {5064, L"ERROR_CLUSTER_NETWORK_ALREADY_OFFLINE"},
+ {5065, L"ERROR_CLUSTER_NODE_ALREADY_MEMBER"},
+ {5066, L"ERROR_CLUSTER_LAST_INTERNAL_NETWORK"},
+ {5067, L"ERROR_CLUSTER_NETWORK_HAS_DEPENDENTS"},
+ {5068, L"ERROR_INVALID_OPERATION_ON_QUORUM"},
+ {5069, L"ERROR_DEPENDENCY_NOT_ALLOWED"},
+ {5070, L"ERROR_CLUSTER_NODE_PAUSED"},
+ {5071, L"ERROR_NODE_CANT_HOST_RESOURCE"},
+ {5072, L"ERROR_CLUSTER_NODE_NOT_READY"},
+ {5073, L"ERROR_CLUSTER_NODE_SHUTTING_DOWN"},
+ {5074, L"ERROR_CLUSTER_JOIN_ABORTED"},
+ {5075, L"ERROR_CLUSTER_INCOMPATIBLE_VERSIONS"},
+ {5076, L"ERROR_CLUSTER_MAXNUM_OF_RESOURCES_EXCEEDED"},
+ {5077, L"ERROR_CLUSTER_SYSTEM_CONFIG_CHANGED"},
+ {5078, L"ERROR_CLUSTER_RESOURCE_TYPE_NOT_FOUND"},
+ {5079, L"ERROR_CLUSTER_RESTYPE_NOT_SUPPORTED"},
+ {5080, L"ERROR_CLUSTER_RESNAME_NOT_FOUND"},
+ {5081, L"ERROR_CLUSTER_NO_RPC_PACKAGES_REGISTERED"},
+ {5082, L"ERROR_CLUSTER_OWNER_NOT_IN_PREFLIST"},
+ {5083, L"ERROR_CLUSTER_DATABASE_SEQMISMATCH"},
+ {5084, L"ERROR_RESMON_INVALID_STATE"},
+ {5085, L"ERROR_CLUSTER_GUM_NOT_LOCKER"},
+ {5086, L"ERROR_QUORUM_DISK_NOT_FOUND"},
+ {5087, L"ERROR_DATABASE_BACKUP_CORRUPT"},
+ {5088, L"ERROR_CLUSTER_NODE_ALREADY_HAS_DFS_ROOT"},
+ {5089, L"ERROR_RESOURCE_PROPERTY_UNCHANGEABLE"},
+ {5890, L"ERROR_CLUSTER_MEMBERSHIP_INVALID_STATE"},
+ {5891, L"ERROR_CLUSTER_QUORUMLOG_NOT_FOUND"},
+ {5892, L"ERROR_CLUSTER_MEMBERSHIP_HALT"},
+ {5893, L"ERROR_CLUSTER_INSTANCE_ID_MISMATCH"},
+ {5894, L"ERROR_CLUSTER_NETWORK_NOT_FOUND_FOR_IP"},
+ {5895, L"ERROR_CLUSTER_PROPERTY_DATA_TYPE_MISMATCH"},
+ {5896, L"ERROR_CLUSTER_EVICT_WITHOUT_CLEANUP"},
+ {5897, L"ERROR_CLUSTER_PARAMETER_MISMATCH"},
+ {5898, L"ERROR_NODE_CANNOT_BE_CLUSTERED"},
+ {5899, L"ERROR_CLUSTER_WRONG_OS_VERSION"},
+ {5900, L"ERROR_CLUSTER_CANT_CREATE_DUP_CLUSTER_NAME"},
+ {5901, L"ERROR_CLUSCFG_ALREADY_COMMITTED"},
+ {5902, L"ERROR_CLUSCFG_ROLLBACK_FAILED"},
+ {5903, L"ERROR_CLUSCFG_SYSTEM_DISK_DRIVE_LETTER_CONFLICT"},
+ {5904, L"ERROR_CLUSTER_OLD_VERSION"},
+ {5905, L"ERROR_CLUSTER_MISMATCHED_COMPUTER_ACCT_NAME"},
+ {5906, L"ERROR_CLUSTER_NO_NET_ADAPTERS"},
+ {5907, L"ERROR_CLUSTER_POISONED"},
+ {5908, L"ERROR_CLUSTER_GROUP_MOVING"},
+ {5909, L"ERROR_CLUSTER_RESOURCE_TYPE_BUSY"},
+ {5910, L"ERROR_RESOURCE_CALL_TIMED_OUT"},
+ {5911, L"ERROR_INVALID_CLUSTER_IPV6_ADDRESS"},
+ {5912, L"ERROR_CLUSTER_INTERNAL_INVALID_FUNCTION"},
+ {5913, L"ERROR_CLUSTER_PARAMETER_OUT_OF_BOUNDS"},
+ {5914, L"ERROR_CLUSTER_PARTIAL_SEND"},
+ {5915, L"ERROR_CLUSTER_REGISTRY_INVALID_FUNCTION"},
+ {5916, L"ERROR_CLUSTER_INVALID_STRING_TERMINATION"},
+ {5917, L"ERROR_CLUSTER_INVALID_STRING_FORMAT"},
+ {5918, L"ERROR_CLUSTER_DATABASE_TRANSACTION_IN_PROGRESS"},
+ {5919, L"ERROR_CLUSTER_DATABASE_TRANSACTION_NOT_IN_PROGRESS"},
+ {5920, L"ERROR_CLUSTER_NULL_DATA"},
+ {5921, L"ERROR_CLUSTER_PARTIAL_READ"},
+ {5922, L"ERROR_CLUSTER_PARTIAL_WRITE"},
+ {5923, L"ERROR_CLUSTER_CANT_DESERIALIZE_DATA"},
+ {5924, L"ERROR_DEPENDENT_RESOURCE_PROPERTY_CONFLICT"},
+ {5925, L"ERROR_CLUSTER_NO_QUORUM"},
+ {5926, L"ERROR_CLUSTER_INVALID_IPV6_NETWORK"},
+ {5927, L"ERROR_CLUSTER_INVALID_IPV6_TUNNEL_NETWORK"},
+ {5928, L"ERROR_QUORUM_NOT_ALLOWED_IN_THIS_GROUP"},
+ {5929, L"ERROR_DEPENDENCY_TREE_TOO_COMPLEX"},
+ {5930, L"ERROR_EXCEPTION_IN_RESOURCE_CALL"},
+ {5931, L"ERROR_CLUSTER_RHS_FAILED_INITIALIZATION"},
+ {5932, L"ERROR_CLUSTER_NOT_INSTALLED"},
+ {5933, L"ERROR_CLUSTER_RESOURCES_MUST_BE_ONLINE_ON_THE_SAME_NODE"},
+ {5934, L"ERROR_CLUSTER_MAX_NODES_IN_CLUSTER"},
+ {5935, L"ERROR_CLUSTER_TOO_MANY_NODES"},
+ {5936, L"ERROR_CLUSTER_OBJECT_ALREADY_USED"},
+ {5937, L"ERROR_NONCORE_GROUPS_FOUND"},
+ {5938, L"ERROR_FILE_SHARE_RESOURCE_CONFLICT"},
+ {5939, L"ERROR_CLUSTER_EVICT_INVALID_REQUEST"},
+ {5940, L"ERROR_CLUSTER_SINGLETON_RESOURCE"},
+ {5941, L"ERROR_CLUSTER_GROUP_SINGLETON_RESOURCE"},
+ {5942, L"ERROR_CLUSTER_RESOURCE_PROVIDER_FAILED"},
+ {5943, L"ERROR_CLUSTER_RESOURCE_CONFIGURATION_ERROR"},
+ {5944, L"ERROR_CLUSTER_GROUP_BUSY"},
+ {5945, L"ERROR_CLUSTER_NOT_SHARED_VOLUME"},
+ {5946, L"ERROR_CLUSTER_INVALID_SECURITY_DESCRIPTOR"},
+ {5947, L"ERROR_CLUSTER_SHARED_VOLUMES_IN_USE"},
+ {5948, L"ERROR_CLUSTER_USE_SHARED_VOLUMES_API"},
+ {5949, L"ERROR_CLUSTER_BACKUP_IN_PROGRESS"},
+ {5950, L"ERROR_NON_CSV_PATH"},
+ {5951, L"ERROR_CSV_VOLUME_NOT_LOCAL"},
+ {5952, L"ERROR_CLUSTER_WATCHDOG_TERMINATING"},
+ {5953, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_INCOMPATIBLE_NODES"},
+ {5954, L"ERROR_CLUSTER_INVALID_NODE_WEIGHT"},
+ {5955, L"ERROR_CLUSTER_RESOURCE_VETOED_CALL"},
+ {5956, L"ERROR_RESMON_SYSTEM_RESOURCES_LACKING"},
+ {5957, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_NOT_ENOUGH_RESOURCES_ON_DESTINATION"},
+ {5958, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_NOT_ENOUGH_RESOURCES_ON_SOURCE"},
+ {5959, L"ERROR_CLUSTER_GROUP_QUEUED"},
+ {5960, L"ERROR_CLUSTER_RESOURCE_LOCKED_STATUS"},
+ {5961, L"ERROR_CLUSTER_SHARED_VOLUME_FAILOVER_NOT_ALLOWED"},
+ {5962, L"ERROR_CLUSTER_NODE_DRAIN_IN_PROGRESS"},
+ {5963, L"ERROR_CLUSTER_DISK_NOT_CONNECTED"},
+ {5964, L"ERROR_DISK_NOT_CSV_CAPABLE"},
+ {5965, L"ERROR_RESOURCE_NOT_IN_AVAILABLE_STORAGE"},
+ {5966, L"ERROR_CLUSTER_SHARED_VOLUME_REDIRECTED"},
+ {5967, L"ERROR_CLUSTER_SHARED_VOLUME_NOT_REDIRECTED"},
+ {5968, L"ERROR_CLUSTER_CANNOT_RETURN_PROPERTIES"},
+ {5969, L"ERROR_CLUSTER_RESOURCE_CONTAINS_UNSUPPORTED_DIFF_AREA_FOR_SHARED_VOLUMES"},
+ {5970, L"ERROR_CLUSTER_RESOURCE_IS_IN_MAINTENANCE_MODE"},
+ {5971, L"ERROR_CLUSTER_AFFINITY_CONFLICT"},
+ {5972, L"ERROR_CLUSTER_RESOURCE_IS_REPLICA_VIRTUAL_MACHINE"},
+ {6000, L"ERROR_ENCRYPTION_FAILED"},
+ {6001, L"ERROR_DECRYPTION_FAILED"},
+ {6002, L"ERROR_FILE_ENCRYPTED"},
+ {6003, L"ERROR_NO_RECOVERY_POLICY"},
+ {6004, L"ERROR_NO_EFS"},
+ {6005, L"ERROR_WRONG_EFS"},
+ {6006, L"ERROR_NO_USER_KEYS"},
+ {6007, L"ERROR_FILE_NOT_ENCRYPTED"},
+ {6008, L"ERROR_NOT_EXPORT_FORMAT"},
+ {6009, L"ERROR_FILE_READ_ONLY"},
+ {6010, L"ERROR_DIR_EFS_DISALLOWED"},
+ {6011, L"ERROR_EFS_SERVER_NOT_TRUSTED"},
+ {6012, L"ERROR_BAD_RECOVERY_POLICY"},
+ {6013, L"ERROR_EFS_ALG_BLOB_TOO_BIG"},
+ {6014, L"ERROR_VOLUME_NOT_SUPPORT_EFS"},
+ {6015, L"ERROR_EFS_DISABLED"},
+ {6016, L"ERROR_EFS_VERSION_NOT_SUPPORT"},
+ {6017, L"ERROR_CS_ENCRYPTION_INVALID_SERVER_RESPONSE"},
+ {6018, L"ERROR_CS_ENCRYPTION_UNSUPPORTED_SERVER"},
+ {6019, L"ERROR_CS_ENCRYPTION_EXISTING_ENCRYPTED_FILE"},
+ {6020, L"ERROR_CS_ENCRYPTION_NEW_ENCRYPTED_FILE"},
+ {6021, L"ERROR_CS_ENCRYPTION_FILE_NOT_CSE"},
+ {6022, L"ERROR_ENCRYPTION_POLICY_DENIES_OPERATION"},
+ {6118, L"ERROR_NO_BROWSER_SERVERS_FOUND"},
+ {6200, L"SCHED_E_SERVICE_NOT_LOCALSYSTEM"},
+ {6600, L"ERROR_LOG_SECTOR_INVALID"},
+ {6601, L"ERROR_LOG_SECTOR_PARITY_INVALID"},
+ {6602, L"ERROR_LOG_SECTOR_REMAPPED"},
+ {6603, L"ERROR_LOG_BLOCK_INCOMPLETE"},
+ {6604, L"ERROR_LOG_INVALID_RANGE"},
+ {6605, L"ERROR_LOG_BLOCKS_EXHAUSTED"},
+ {6606, L"ERROR_LOG_READ_CONTEXT_INVALID"},
+ {6607, L"ERROR_LOG_RESTART_INVALID"},
+ {6608, L"ERROR_LOG_BLOCK_VERSION"},
+ {6609, L"ERROR_LOG_BLOCK_INVALID"},
+ {6610, L"ERROR_LOG_READ_MODE_INVALID"},
+ {6611, L"ERROR_LOG_NO_RESTART"},
+ {6612, L"ERROR_LOG_METADATA_CORRUPT"},
+ {6613, L"ERROR_LOG_METADATA_INVALID"},
+ {6614, L"ERROR_LOG_METADATA_INCONSISTENT"},
+ {6615, L"ERROR_LOG_RESERVATION_INVALID"},
+ {6616, L"ERROR_LOG_CANT_DELETE"},
+ {6617, L"ERROR_LOG_CONTAINER_LIMIT_EXCEEDED"},
+ {6618, L"ERROR_LOG_START_OF_LOG"},
+ {6619, L"ERROR_LOG_POLICY_ALREADY_INSTALLED"},
+ {6620, L"ERROR_LOG_POLICY_NOT_INSTALLED"},
+ {6621, L"ERROR_LOG_POLICY_INVALID"},
+ {6622, L"ERROR_LOG_POLICY_CONFLICT"},
+ {6623, L"ERROR_LOG_PINNED_ARCHIVE_TAIL"},
+ {6624, L"ERROR_LOG_RECORD_NONEXISTENT"},
+ {6625, L"ERROR_LOG_RECORDS_RESERVED_INVALID"},
+ {6626, L"ERROR_LOG_SPACE_RESERVED_INVALID"},
+ {6627, L"ERROR_LOG_TAIL_INVALID"},
+ {6628, L"ERROR_LOG_FULL"},
+ {6629, L"ERROR_COULD_NOT_RESIZE_LOG"},
+ {6630, L"ERROR_LOG_MULTIPLEXED"},
+ {6631, L"ERROR_LOG_DEDICATED"},
+ {6632, L"ERROR_LOG_ARCHIVE_NOT_IN_PROGRESS"},
+ {6633, L"ERROR_LOG_ARCHIVE_IN_PROGRESS"},
+ {6634, L"ERROR_LOG_EPHEMERAL"},
+ {6635, L"ERROR_LOG_NOT_ENOUGH_CONTAINERS"},
+ {6636, L"ERROR_LOG_CLIENT_ALREADY_REGISTERED"},
+ {6637, L"ERROR_LOG_CLIENT_NOT_REGISTERED"},
+ {6638, L"ERROR_LOG_FULL_HANDLER_IN_PROGRESS"},
+ {6639, L"ERROR_LOG_CONTAINER_READ_FAILED"},
+ {6640, L"ERROR_LOG_CONTAINER_WRITE_FAILED"},
+ {6641, L"ERROR_LOG_CONTAINER_OPEN_FAILED"},
+ {6642, L"ERROR_LOG_CONTAINER_STATE_INVALID"},
+ {6643, L"ERROR_LOG_STATE_INVALID"},
+ {6644, L"ERROR_LOG_PINNED"},
+ {6645, L"ERROR_LOG_METADATA_FLUSH_FAILED"},
+ {6646, L"ERROR_LOG_INCONSISTENT_SECURITY"},
+ {6647, L"ERROR_LOG_APPENDED_FLUSH_FAILED"},
+ {6648, L"ERROR_LOG_PINNED_RESERVATION"},
+ {6700, L"ERROR_INVALID_TRANSACTION"},
+ {6701, L"ERROR_TRANSACTION_NOT_ACTIVE"},
+ {6702, L"ERROR_TRANSACTION_REQUEST_NOT_VALID"},
+ {6703, L"ERROR_TRANSACTION_NOT_REQUESTED"},
+ {6704, L"ERROR_TRANSACTION_ALREADY_ABORTED"},
+ {6705, L"ERROR_TRANSACTION_ALREADY_COMMITTED"},
+ {6706, L"ERROR_TM_INITIALIZATION_FAILED"},
+ {6707, L"ERROR_RESOURCEMANAGER_READ_ONLY"},
+ {6708, L"ERROR_TRANSACTION_NOT_JOINED"},
+ {6709, L"ERROR_TRANSACTION_SUPERIOR_EXISTS"},
+ {6710, L"ERROR_CRM_PROTOCOL_ALREADY_EXISTS"},
+ {6711, L"ERROR_TRANSACTION_PROPAGATION_FAILED"},
+ {6712, L"ERROR_CRM_PROTOCOL_NOT_FOUND"},
+ {6713, L"ERROR_TRANSACTION_INVALID_MARSHALL_BUFFER"},
+ {6714, L"ERROR_CURRENT_TRANSACTION_NOT_VALID"},
+ {6715, L"ERROR_TRANSACTION_NOT_FOUND"},
+ {6716, L"ERROR_RESOURCEMANAGER_NOT_FOUND"},
+ {6717, L"ERROR_ENLISTMENT_NOT_FOUND"},
+ {6718, L"ERROR_TRANSACTIONMANAGER_NOT_FOUND"},
+ {6719, L"ERROR_TRANSACTIONMANAGER_NOT_ONLINE"},
+ {6720, L"ERROR_TRANSACTIONMANAGER_RECOVERY_NAME_COLLISION"},
+ {6721, L"ERROR_TRANSACTION_NOT_ROOT"},
+ {6722, L"ERROR_TRANSACTION_OBJECT_EXPIRED"},
+ {6723, L"ERROR_TRANSACTION_RESPONSE_NOT_ENLISTED"},
+ {6724, L"ERROR_TRANSACTION_RECORD_TOO_LONG"},
+ {6725, L"ERROR_IMPLICIT_TRANSACTION_NOT_SUPPORTED"},
+ {6726, L"ERROR_TRANSACTION_INTEGRITY_VIOLATED"},
+ {6727, L"ERROR_TRANSACTIONMANAGER_IDENTITY_MISMATCH"},
+ {6728, L"ERROR_RM_CANNOT_BE_FROZEN_FOR_SNAPSHOT"},
+ {6729, L"ERROR_TRANSACTION_MUST_WRITETHROUGH"},
+ {6730, L"ERROR_TRANSACTION_NO_SUPERIOR"},
+ {6731, L"ERROR_HEURISTIC_DAMAGE_POSSIBLE"},
+ {6800, L"ERROR_TRANSACTIONAL_CONFLICT"},
+ {6801, L"ERROR_RM_NOT_ACTIVE"},
+ {6802, L"ERROR_RM_METADATA_CORRUPT"},
+ {6803, L"ERROR_DIRECTORY_NOT_RM"},
+ {6805, L"ERROR_TRANSACTIONS_UNSUPPORTED_REMOTE"},
+ {6806, L"ERROR_LOG_RESIZE_INVALID_SIZE"},
+ {6807, L"ERROR_OBJECT_NO_LONGER_EXISTS"},
+ {6808, L"ERROR_STREAM_MINIVERSION_NOT_FOUND"},
+ {6809, L"ERROR_STREAM_MINIVERSION_NOT_VALID"},
+ {6810, L"ERROR_MINIVERSION_INACCESSIBLE_FROM_SPECIFIED_TRANSACTION"},
+ {6811, L"ERROR_CANT_OPEN_MINIVERSION_WITH_MODIFY_INTENT"},
+ {6812, L"ERROR_CANT_CREATE_MORE_STREAM_MINIVERSIONS"},
+ {6814, L"ERROR_REMOTE_FILE_VERSION_MISMATCH"},
+ {6815, L"ERROR_HANDLE_NO_LONGER_VALID"},
+ {6816, L"ERROR_NO_TXF_METADATA"},
+ {6817, L"ERROR_LOG_CORRUPTION_DETECTED"},
+ {6818, L"ERROR_CANT_RECOVER_WITH_HANDLE_OPEN"},
+ {6819, L"ERROR_RM_DISCONNECTED"},
+ {6820, L"ERROR_ENLISTMENT_NOT_SUPERIOR"},
+ {6821, L"ERROR_RECOVERY_NOT_NEEDED"},
+ {6822, L"ERROR_RM_ALREADY_STARTED"},
+ {6823, L"ERROR_FILE_IDENTITY_NOT_PERSISTENT"},
+ {6824, L"ERROR_CANT_BREAK_TRANSACTIONAL_DEPENDENCY"},
+ {6825, L"ERROR_CANT_CROSS_RM_BOUNDARY"},
+ {6826, L"ERROR_TXF_DIR_NOT_EMPTY"},
+ {6827, L"ERROR_INDOUBT_TRANSACTIONS_EXIST"},
+ {6828, L"ERROR_TM_VOLATILE"},
+ {6829, L"ERROR_ROLLBACK_TIMER_EXPIRED"},
+ {6830, L"ERROR_TXF_ATTRIBUTE_CORRUPT"},
+ {6831, L"ERROR_EFS_NOT_ALLOWED_IN_TRANSACTION"},
+ {6832, L"ERROR_TRANSACTIONAL_OPEN_NOT_ALLOWED"},
+ {6833, L"ERROR_LOG_GROWTH_FAILED"},
+ {6834, L"ERROR_TRANSACTED_MAPPING_UNSUPPORTED_REMOTE"},
+ {6835, L"ERROR_TXF_METADATA_ALREADY_PRESENT"},
+ {6836, L"ERROR_TRANSACTION_SCOPE_CALLBACKS_NOT_SET"},
+ {6837, L"ERROR_TRANSACTION_REQUIRED_PROMOTION"},
+ {6838, L"ERROR_CANNOT_EXECUTE_FILE_IN_TRANSACTION"},
+ {6839, L"ERROR_TRANSACTIONS_NOT_FROZEN"},
+ {6840, L"ERROR_TRANSACTION_FREEZE_IN_PROGRESS"},
+ {6841, L"ERROR_NOT_SNAPSHOT_VOLUME"},
+ {6842, L"ERROR_NO_SAVEPOINT_WITH_OPEN_FILES"},
+ {6843, L"ERROR_DATA_LOST_REPAIR"},
+ {6844, L"ERROR_SPARSE_NOT_ALLOWED_IN_TRANSACTION"},
+ {6845, L"ERROR_TM_IDENTITY_MISMATCH"},
+ {6846, L"ERROR_FLOATED_SECTION"},
+ {6847, L"ERROR_CANNOT_ACCEPT_TRANSACTED_WORK"},
+ {6848, L"ERROR_CANNOT_ABORT_TRANSACTIONS"},
+ {6849, L"ERROR_BAD_CLUSTERS"},
+ {6850, L"ERROR_COMPRESSION_NOT_ALLOWED_IN_TRANSACTION"},
+ {6851, L"ERROR_VOLUME_DIRTY"},
+ {6852, L"ERROR_NO_LINK_TRACKING_IN_TRANSACTION"},
+ {6853, L"ERROR_OPERATION_NOT_SUPPORTED_IN_TRANSACTION"},
+ {6854, L"ERROR_EXPIRED_HANDLE"},
+ {6855, L"ERROR_TRANSACTION_NOT_ENLISTED"},
+ {7001, L"ERROR_CTX_WINSTATION_NAME_INVALID"},
+ {7002, L"ERROR_CTX_INVALID_PD"},
+ {7003, L"ERROR_CTX_PD_NOT_FOUND"},
+ {7004, L"ERROR_CTX_WD_NOT_FOUND"},
+ {7005, L"ERROR_CTX_CANNOT_MAKE_EVENTLOG_ENTRY"},
+ {7006, L"ERROR_CTX_SERVICE_NAME_COLLISION"},
+ {7007, L"ERROR_CTX_CLOSE_PENDING"},
+ {7008, L"ERROR_CTX_NO_OUTBUF"},
+ {7009, L"ERROR_CTX_MODEM_INF_NOT_FOUND"},
+ {7010, L"ERROR_CTX_INVALID_MODEMNAME"},
+ {7011, L"ERROR_CTX_MODEM_RESPONSE_ERROR"},
+ {7012, L"ERROR_CTX_MODEM_RESPONSE_TIMEOUT"},
+ {7013, L"ERROR_CTX_MODEM_RESPONSE_NO_CARRIER"},
+ {7014, L"ERROR_CTX_MODEM_RESPONSE_NO_DIALTONE"},
+ {7015, L"ERROR_CTX_MODEM_RESPONSE_BUSY"},
+ {7016, L"ERROR_CTX_MODEM_RESPONSE_VOICE"},
+ {7017, L"ERROR_CTX_TD_ERROR"},
+ {7022, L"ERROR_CTX_WINSTATION_NOT_FOUND"},
+ {7023, L"ERROR_CTX_WINSTATION_ALREADY_EXISTS"},
+ {7024, L"ERROR_CTX_WINSTATION_BUSY"},
+ {7025, L"ERROR_CTX_BAD_VIDEO_MODE"},
+ {7035, L"ERROR_CTX_GRAPHICS_INVALID"},
+ {7037, L"ERROR_CTX_LOGON_DISABLED"},
+ {7038, L"ERROR_CTX_NOT_CONSOLE"},
+ {7040, L"ERROR_CTX_CLIENT_QUERY_TIMEOUT"},
+ {7041, L"ERROR_CTX_CONSOLE_DISCONNECT"},
+ {7042, L"ERROR_CTX_CONSOLE_CONNECT"},
+ {7044, L"ERROR_CTX_SHADOW_DENIED"},
+ {7045, L"ERROR_CTX_WINSTATION_ACCESS_DENIED"},
+ {7049, L"ERROR_CTX_INVALID_WD"},
+ {7050, L"ERROR_CTX_SHADOW_INVALID"},
+ {7051, L"ERROR_CTX_SHADOW_DISABLED"},
+ {7052, L"ERROR_CTX_CLIENT_LICENSE_IN_USE"},
+ {7053, L"ERROR_CTX_CLIENT_LICENSE_NOT_SET"},
+ {7054, L"ERROR_CTX_LICENSE_NOT_AVAILABLE"},
+ {7055, L"ERROR_CTX_LICENSE_CLIENT_INVALID"},
+ {7056, L"ERROR_CTX_LICENSE_EXPIRED"},
+ {7057, L"ERROR_CTX_SHADOW_NOT_RUNNING"},
+ {7058, L"ERROR_CTX_SHADOW_ENDED_BY_MODE_CHANGE"},
+ {7059, L"ERROR_ACTIVATION_COUNT_EXCEEDED"},
+ {7060, L"ERROR_CTX_WINSTATIONS_DISABLED"},
+ {7061, L"ERROR_CTX_ENCRYPTION_LEVEL_REQUIRED"},
+ {7062, L"ERROR_CTX_SESSION_IN_USE"},
+ {7063, L"ERROR_CTX_NO_FORCE_LOGOFF"},
+ {7064, L"ERROR_CTX_ACCOUNT_RESTRICTION"},
+ {7065, L"ERROR_RDP_PROTOCOL_ERROR"},
+ {7066, L"ERROR_CTX_CDM_CONNECT"},
+ {7067, L"ERROR_CTX_CDM_DISCONNECT"},
+ {7068, L"ERROR_CTX_SECURITY_LAYER_ERROR"},
+ {7069, L"ERROR_TS_INCOMPATIBLE_SESSIONS"},
+ {7070, L"ERROR_TS_VIDEO_SUBSYSTEM_ERROR"},
+ {8001, L"FRS_ERR_INVALID_API_SEQUENCE"},
+ {8002, L"FRS_ERR_STARTING_SERVICE"},
+ {8003, L"FRS_ERR_STOPPING_SERVICE"},
+ {8004, L"FRS_ERR_INTERNAL_API"},
+ {8005, L"FRS_ERR_INTERNAL"},
+ {8006, L"FRS_ERR_SERVICE_COMM"},
+ {8007, L"FRS_ERR_INSUFFICIENT_PRIV"},
+ {8008, L"FRS_ERR_AUTHENTICATION"},
+ {8009, L"FRS_ERR_PARENT_INSUFFICIENT_PRIV"},
+ {8010, L"FRS_ERR_PARENT_AUTHENTICATION"},
+ {8011, L"FRS_ERR_CHILD_TO_PARENT_COMM"},
+ {8012, L"FRS_ERR_PARENT_TO_CHILD_COMM"},
+ {8013, L"FRS_ERR_SYSVOL_POPULATE"},
+ {8014, L"FRS_ERR_SYSVOL_POPULATE_TIMEOUT"},
+ {8015, L"FRS_ERR_SYSVOL_IS_BUSY"},
+ {8016, L"FRS_ERR_SYSVOL_DEMOTE"},
+ {8017, L"FRS_ERR_INVALID_SERVICE_PARAMETER"},
+ {8200, L"ERROR_DS_NOT_INSTALLED"},
+ {8201, L"ERROR_DS_MEMBERSHIP_EVALUATED_LOCALLY"},
+ {8202, L"ERROR_DS_NO_ATTRIBUTE_OR_VALUE"},
+ {8203, L"ERROR_DS_INVALID_ATTRIBUTE_SYNTAX"},
+ {8204, L"ERROR_DS_ATTRIBUTE_TYPE_UNDEFINED"},
+ {8205, L"ERROR_DS_ATTRIBUTE_OR_VALUE_EXISTS"},
+ {8206, L"ERROR_DS_BUSY"},
+ {8207, L"ERROR_DS_UNAVAILABLE"},
+ {8208, L"ERROR_DS_NO_RIDS_ALLOCATED"},
+ {8209, L"ERROR_DS_NO_MORE_RIDS"},
+ {8210, L"ERROR_DS_INCORRECT_ROLE_OWNER"},
+ {8211, L"ERROR_DS_RIDMGR_INIT_ERROR"},
+ {8212, L"ERROR_DS_OBJ_CLASS_VIOLATION"},
+ {8213, L"ERROR_DS_CANT_ON_NON_LEAF"},
+ {8214, L"ERROR_DS_CANT_ON_RDN"},
+ {8215, L"ERROR_DS_CANT_MOD_OBJ_CLASS"},
+ {8216, L"ERROR_DS_CROSS_DOM_MOVE_ERROR"},
+ {8217, L"ERROR_DS_GC_NOT_AVAILABLE"},
+ {8218, L"ERROR_SHARED_POLICY"},
+ {8219, L"ERROR_POLICY_OBJECT_NOT_FOUND"},
+ {8220, L"ERROR_POLICY_ONLY_IN_DS"},
+ {8221, L"ERROR_PROMOTION_ACTIVE"},
+ {8222, L"ERROR_NO_PROMOTION_ACTIVE"},
+ {8224, L"ERROR_DS_OPERATIONS_ERROR"},
+ {8225, L"ERROR_DS_PROTOCOL_ERROR"},
+ {8226, L"ERROR_DS_TIMELIMIT_EXCEEDED"},
+ {8227, L"ERROR_DS_SIZELIMIT_EXCEEDED"},
+ {8228, L"ERROR_DS_ADMIN_LIMIT_EXCEEDED"},
+ {8229, L"ERROR_DS_COMPARE_FALSE"},
+ {8230, L"ERROR_DS_COMPARE_TRUE"},
+ {8231, L"ERROR_DS_AUTH_METHOD_NOT_SUPPORTED"},
+ {8232, L"ERROR_DS_STRONG_AUTH_REQUIRED"},
+ {8233, L"ERROR_DS_INAPPROPRIATE_AUTH"},
+ {8234, L"ERROR_DS_AUTH_UNKNOWN"},
+ {8235, L"ERROR_DS_REFERRAL"},
+ {8236, L"ERROR_DS_UNAVAILABLE_CRIT_EXTENSION"},
+ {8237, L"ERROR_DS_CONFIDENTIALITY_REQUIRED"},
+ {8238, L"ERROR_DS_INAPPROPRIATE_MATCHING"},
+ {8239, L"ERROR_DS_CONSTRAINT_VIOLATION"},
+ {8240, L"ERROR_DS_NO_SUCH_OBJECT"},
+ {8241, L"ERROR_DS_ALIAS_PROBLEM"},
+ {8242, L"ERROR_DS_INVALID_DN_SYNTAX"},
+ {8243, L"ERROR_DS_IS_LEAF"},
+ {8244, L"ERROR_DS_ALIAS_DEREF_PROBLEM"},
+ {8245, L"ERROR_DS_UNWILLING_TO_PERFORM"},
+ {8246, L"ERROR_DS_LOOP_DETECT"},
+ {8247, L"ERROR_DS_NAMING_VIOLATION"},
+ {8248, L"ERROR_DS_OBJECT_RESULTS_TOO_LARGE"},
+ {8249, L"ERROR_DS_AFFECTS_MULTIPLE_DSAS"},
+ {8250, L"ERROR_DS_SERVER_DOWN"},
+ {8251, L"ERROR_DS_LOCAL_ERROR"},
+ {8252, L"ERROR_DS_ENCODING_ERROR"},
+ {8253, L"ERROR_DS_DECODING_ERROR"},
+ {8254, L"ERROR_DS_FILTER_UNKNOWN"},
+ {8255, L"ERROR_DS_PARAM_ERROR"},
+ {8256, L"ERROR_DS_NOT_SUPPORTED"},
+ {8257, L"ERROR_DS_NO_RESULTS_RETURNED"},
+ {8258, L"ERROR_DS_CONTROL_NOT_FOUND"},
+ {8259, L"ERROR_DS_CLIENT_LOOP"},
+ {8260, L"ERROR_DS_REFERRAL_LIMIT_EXCEEDED"},
+ {8261, L"ERROR_DS_SORT_CONTROL_MISSING"},
+ {8262, L"ERROR_DS_OFFSET_RANGE_ERROR"},
+ {8263, L"ERROR_DS_RIDMGR_DISABLED"},
+ {8301, L"ERROR_DS_ROOT_MUST_BE_NC"},
+ {8302, L"ERROR_DS_ADD_REPLICA_INHIBITED"},
+ {8303, L"ERROR_DS_ATT_NOT_DEF_IN_SCHEMA"},
+ {8304, L"ERROR_DS_MAX_OBJ_SIZE_EXCEEDED"},
+ {8305, L"ERROR_DS_OBJ_STRING_NAME_EXISTS"},
+ {8306, L"ERROR_DS_NO_RDN_DEFINED_IN_SCHEMA"},
+ {8307, L"ERROR_DS_RDN_DOESNT_MATCH_SCHEMA"},
+ {8308, L"ERROR_DS_NO_REQUESTED_ATTS_FOUND"},
+ {8309, L"ERROR_DS_USER_BUFFER_TO_SMALL"},
+ {8310, L"ERROR_DS_ATT_IS_NOT_ON_OBJ"},
+ {8311, L"ERROR_DS_ILLEGAL_MOD_OPERATION"},
+ {8312, L"ERROR_DS_OBJ_TOO_LARGE"},
+ {8313, L"ERROR_DS_BAD_INSTANCE_TYPE"},
+ {8314, L"ERROR_DS_MASTERDSA_REQUIRED"},
+ {8315, L"ERROR_DS_OBJECT_CLASS_REQUIRED"},
+ {8316, L"ERROR_DS_MISSING_REQUIRED_ATT"},
+ {8317, L"ERROR_DS_ATT_NOT_DEF_FOR_CLASS"},
+ {8318, L"ERROR_DS_ATT_ALREADY_EXISTS"},
+ {8320, L"ERROR_DS_CANT_ADD_ATT_VALUES"},
+ {8321, L"ERROR_DS_SINGLE_VALUE_CONSTRAINT"},
+ {8322, L"ERROR_DS_RANGE_CONSTRAINT"},
+ {8323, L"ERROR_DS_ATT_VAL_ALREADY_EXISTS"},
+ {8324, L"ERROR_DS_CANT_REM_MISSING_ATT"},
+ {8325, L"ERROR_DS_CANT_REM_MISSING_ATT_VAL"},
+ {8326, L"ERROR_DS_ROOT_CANT_BE_SUBREF"},
+ {8327, L"ERROR_DS_NO_CHAINING"},
+ {8328, L"ERROR_DS_NO_CHAINED_EVAL"},
+ {8329, L"ERROR_DS_NO_PARENT_OBJECT"},
+ {8330, L"ERROR_DS_PARENT_IS_AN_ALIAS"},
+ {8331, L"ERROR_DS_CANT_MIX_MASTER_AND_REPS"},
+ {8332, L"ERROR_DS_CHILDREN_EXIST"},
+ {8333, L"ERROR_DS_OBJ_NOT_FOUND"},
+ {8334, L"ERROR_DS_ALIASED_OBJ_MISSING"},
+ {8335, L"ERROR_DS_BAD_NAME_SYNTAX"},
+ {8336, L"ERROR_DS_ALIAS_POINTS_TO_ALIAS"},
+ {8337, L"ERROR_DS_CANT_DEREF_ALIAS"},
+ {8338, L"ERROR_DS_OUT_OF_SCOPE"},
+ {8339, L"ERROR_DS_OBJECT_BEING_REMOVED"},
+ {8340, L"ERROR_DS_CANT_DELETE_DSA_OBJ"},
+ {8341, L"ERROR_DS_GENERIC_ERROR"},
+ {8342, L"ERROR_DS_DSA_MUST_BE_INT_MASTER"},
+ {8343, L"ERROR_DS_CLASS_NOT_DSA"},
+ {8344, L"ERROR_DS_INSUFF_ACCESS_RIGHTS"},
+ {8345, L"ERROR_DS_ILLEGAL_SUPERIOR"},
+ {8346, L"ERROR_DS_ATTRIBUTE_OWNED_BY_SAM"},
+ {8347, L"ERROR_DS_NAME_TOO_MANY_PARTS"},
+ {8348, L"ERROR_DS_NAME_TOO_LONG"},
+ {8349, L"ERROR_DS_NAME_VALUE_TOO_LONG"},
+ {8350, L"ERROR_DS_NAME_UNPARSEABLE"},
+ {8351, L"ERROR_DS_NAME_TYPE_UNKNOWN"},
+ {8352, L"ERROR_DS_NOT_AN_OBJECT"},
+ {8353, L"ERROR_DS_SEC_DESC_TOO_SHORT"},
+ {8354, L"ERROR_DS_SEC_DESC_INVALID"},
+ {8355, L"ERROR_DS_NO_DELETED_NAME"},
+ {8356, L"ERROR_DS_SUBREF_MUST_HAVE_PARENT"},
+ {8357, L"ERROR_DS_NCNAME_MUST_BE_NC"},
+ {8358, L"ERROR_DS_CANT_ADD_SYSTEM_ONLY"},
+ {8359, L"ERROR_DS_CLASS_MUST_BE_CONCRETE"},
+ {8360, L"ERROR_DS_INVALID_DMD"},
+ {8361, L"ERROR_DS_OBJ_GUID_EXISTS"},
+ {8362, L"ERROR_DS_NOT_ON_BACKLINK"},
+ {8363, L"ERROR_DS_NO_CROSSREF_FOR_NC"},
+ {8364, L"ERROR_DS_SHUTTING_DOWN"},
+ {8365, L"ERROR_DS_UNKNOWN_OPERATION"},
+ {8366, L"ERROR_DS_INVALID_ROLE_OWNER"},
+ {8367, L"ERROR_DS_COULDNT_CONTACT_FSMO"},
+ {8368, L"ERROR_DS_CROSS_NC_DN_RENAME"},
+ {8369, L"ERROR_DS_CANT_MOD_SYSTEM_ONLY"},
+ {8370, L"ERROR_DS_REPLICATOR_ONLY"},
+ {8371, L"ERROR_DS_OBJ_CLASS_NOT_DEFINED"},
+ {8372, L"ERROR_DS_OBJ_CLASS_NOT_SUBCLASS"},
+ {8373, L"ERROR_DS_NAME_REFERENCE_INVALID"},
+ {8374, L"ERROR_DS_CROSS_REF_EXISTS"},
+ {8375, L"ERROR_DS_CANT_DEL_MASTER_CROSSREF"},
+ {8376, L"ERROR_DS_SUBTREE_NOTIFY_NOT_NC_HEAD"},
+ {8377, L"ERROR_DS_NOTIFY_FILTER_TOO_COMPLEX"},
+ {8378, L"ERROR_DS_DUP_RDN"},
+ {8379, L"ERROR_DS_DUP_OID"},
+ {8380, L"ERROR_DS_DUP_MAPI_ID"},
+ {8381, L"ERROR_DS_DUP_SCHEMA_ID_GUID"},
+ {8382, L"ERROR_DS_DUP_LDAP_DISPLAY_NAME"},
+ {8383, L"ERROR_DS_SEMANTIC_ATT_TEST"},
+ {8384, L"ERROR_DS_SYNTAX_MISMATCH"},
+ {8385, L"ERROR_DS_EXISTS_IN_MUST_HAVE"},
+ {8386, L"ERROR_DS_EXISTS_IN_MAY_HAVE"},
+ {8387, L"ERROR_DS_NONEXISTENT_MAY_HAVE"},
+ {8388, L"ERROR_DS_NONEXISTENT_MUST_HAVE"},
+ {8389, L"ERROR_DS_AUX_CLS_TEST_FAIL"},
+ {8390, L"ERROR_DS_NONEXISTENT_POSS_SUP"},
+ {8391, L"ERROR_DS_SUB_CLS_TEST_FAIL"},
+ {8392, L"ERROR_DS_BAD_RDN_ATT_ID_SYNTAX"},
+ {8393, L"ERROR_DS_EXISTS_IN_AUX_CLS"},
+ {8394, L"ERROR_DS_EXISTS_IN_SUB_CLS"},
+ {8395, L"ERROR_DS_EXISTS_IN_POSS_SUP"},
+ {8396, L"ERROR_DS_RECALCSCHEMA_FAILED"},
+ {8397, L"ERROR_DS_TREE_DELETE_NOT_FINISHED"},
+ {8398, L"ERROR_DS_CANT_DELETE"},
+ {8399, L"ERROR_DS_ATT_SCHEMA_REQ_ID"},
+ {8400, L"ERROR_DS_BAD_ATT_SCHEMA_SYNTAX"},
+ {8401, L"ERROR_DS_CANT_CACHE_ATT"},
+ {8402, L"ERROR_DS_CANT_CACHE_CLASS"},
+ {8403, L"ERROR_DS_CANT_REMOVE_ATT_CACHE"},
+ {8404, L"ERROR_DS_CANT_REMOVE_CLASS_CACHE"},
+ {8405, L"ERROR_DS_CANT_RETRIEVE_DN"},
+ {8406, L"ERROR_DS_MISSING_SUPREF"},
+ {8407, L"ERROR_DS_CANT_RETRIEVE_INSTANCE"},
+ {8408, L"ERROR_DS_CODE_INCONSISTENCY"},
+ {8409, L"ERROR_DS_DATABASE_ERROR"},
+ {8410, L"ERROR_DS_GOVERNSID_MISSING"},
+ {8411, L"ERROR_DS_MISSING_EXPECTED_ATT"},
+ {8412, L"ERROR_DS_NCNAME_MISSING_CR_REF"},
+ {8413, L"ERROR_DS_SECURITY_CHECKING_ERROR"},
+ {8414, L"ERROR_DS_SCHEMA_NOT_LOADED"},
+ {8415, L"ERROR_DS_SCHEMA_ALLOC_FAILED"},
+ {8416, L"ERROR_DS_ATT_SCHEMA_REQ_SYNTAX"},
+ {8417, L"ERROR_DS_GCVERIFY_ERROR"},
+ {8418, L"ERROR_DS_DRA_SCHEMA_MISMATCH"},
+ {8419, L"ERROR_DS_CANT_FIND_DSA_OBJ"},
+ {8420, L"ERROR_DS_CANT_FIND_EXPECTED_NC"},
+ {8421, L"ERROR_DS_CANT_FIND_NC_IN_CACHE"},
+ {8422, L"ERROR_DS_CANT_RETRIEVE_CHILD"},
+ {8423, L"ERROR_DS_SECURITY_ILLEGAL_MODIFY"},
+ {8424, L"ERROR_DS_CANT_REPLACE_HIDDEN_REC"},
+ {8425, L"ERROR_DS_BAD_HIERARCHY_FILE"},
+ {8426, L"ERROR_DS_BUILD_HIERARCHY_TABLE_FAILED"},
+ {8427, L"ERROR_DS_CONFIG_PARAM_MISSING"},
+ {8428, L"ERROR_DS_COUNTING_AB_INDICES_FAILED"},
+ {8429, L"ERROR_DS_HIERARCHY_TABLE_MALLOC_FAILED"},
+ {8430, L"ERROR_DS_INTERNAL_FAILURE"},
+ {8431, L"ERROR_DS_UNKNOWN_ERROR"},
+ {8432, L"ERROR_DS_ROOT_REQUIRES_CLASS_TOP"},
+ {8433, L"ERROR_DS_REFUSING_FSMO_ROLES"},
+ {8434, L"ERROR_DS_MISSING_FSMO_SETTINGS"},
+ {8435, L"ERROR_DS_UNABLE_TO_SURRENDER_ROLES"},
+ {8436, L"ERROR_DS_DRA_GENERIC"},
+ {8437, L"ERROR_DS_DRA_INVALID_PARAMETER"},
+ {8438, L"ERROR_DS_DRA_BUSY"},
+ {8439, L"ERROR_DS_DRA_BAD_DN"},
+ {8440, L"ERROR_DS_DRA_BAD_NC"},
+ {8441, L"ERROR_DS_DRA_DN_EXISTS"},
+ {8442, L"ERROR_DS_DRA_INTERNAL_ERROR"},
+ {8443, L"ERROR_DS_DRA_INCONSISTENT_DIT"},
+ {8444, L"ERROR_DS_DRA_CONNECTION_FAILED"},
+ {8445, L"ERROR_DS_DRA_BAD_INSTANCE_TYPE"},
+ {8446, L"ERROR_DS_DRA_OUT_OF_MEM"},
+ {8447, L"ERROR_DS_DRA_MAIL_PROBLEM"},
+ {8448, L"ERROR_DS_DRA_REF_ALREADY_EXISTS"},
+ {8449, L"ERROR_DS_DRA_REF_NOT_FOUND"},
+ {8450, L"ERROR_DS_DRA_OBJ_IS_REP_SOURCE"},
+ {8451, L"ERROR_DS_DRA_DB_ERROR"},
+ {8452, L"ERROR_DS_DRA_NO_REPLICA"},
+ {8453, L"ERROR_DS_DRA_ACCESS_DENIED"},
+ {8454, L"ERROR_DS_DRA_NOT_SUPPORTED"},
+ {8455, L"ERROR_DS_DRA_RPC_CANCELLED"},
+ {8456, L"ERROR_DS_DRA_SOURCE_DISABLED"},
+ {8457, L"ERROR_DS_DRA_SINK_DISABLED"},
+ {8458, L"ERROR_DS_DRA_NAME_COLLISION"},
+ {8459, L"ERROR_DS_DRA_SOURCE_REINSTALLED"},
+ {8460, L"ERROR_DS_DRA_MISSING_PARENT"},
+ {8461, L"ERROR_DS_DRA_PREEMPTED"},
+ {8462, L"ERROR_DS_DRA_ABANDON_SYNC"},
+ {8463, L"ERROR_DS_DRA_SHUTDOWN"},
+ {8464, L"ERROR_DS_DRA_INCOMPATIBLE_PARTIAL_SET"},
+ {8465, L"ERROR_DS_DRA_SOURCE_IS_PARTIAL_REPLICA"},
+ {8466, L"ERROR_DS_DRA_EXTN_CONNECTION_FAILED"},
+ {8467, L"ERROR_DS_INSTALL_SCHEMA_MISMATCH"},
+ {8468, L"ERROR_DS_DUP_LINK_ID"},
+ {8469, L"ERROR_DS_NAME_ERROR_RESOLVING"},
+ {8470, L"ERROR_DS_NAME_ERROR_NOT_FOUND"},
+ {8471, L"ERROR_DS_NAME_ERROR_NOT_UNIQUE"},
+ {8472, L"ERROR_DS_NAME_ERROR_NO_MAPPING"},
+ {8473, L"ERROR_DS_NAME_ERROR_DOMAIN_ONLY"},
+ {8474, L"ERROR_DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING"},
+ {8475, L"ERROR_DS_CONSTRUCTED_ATT_MOD"},
+ {8476, L"ERROR_DS_WRONG_OM_OBJ_CLASS"},
+ {8477, L"ERROR_DS_DRA_REPL_PENDING"},
+ {8478, L"ERROR_DS_DS_REQUIRED"},
+ {8479, L"ERROR_DS_INVALID_LDAP_DISPLAY_NAME"},
+ {8480, L"ERROR_DS_NON_BASE_SEARCH"},
+ {8481, L"ERROR_DS_CANT_RETRIEVE_ATTS"},
+ {8482, L"ERROR_DS_BACKLINK_WITHOUT_LINK"},
+ {8483, L"ERROR_DS_EPOCH_MISMATCH"},
+ {8484, L"ERROR_DS_SRC_NAME_MISMATCH"},
+ {8485, L"ERROR_DS_SRC_AND_DST_NC_IDENTICAL"},
+ {8486, L"ERROR_DS_DST_NC_MISMATCH"},
+ {8487, L"ERROR_DS_NOT_AUTHORITIVE_FOR_DST_NC"},
+ {8488, L"ERROR_DS_SRC_GUID_MISMATCH"},
+ {8489, L"ERROR_DS_CANT_MOVE_DELETED_OBJECT"},
+ {8490, L"ERROR_DS_PDC_OPERATION_IN_PROGRESS"},
+ {8491, L"ERROR_DS_CROSS_DOMAIN_CLEANUP_REQD"},
+ {8492, L"ERROR_DS_ILLEGAL_XDOM_MOVE_OPERATION"},
+ {8493, L"ERROR_DS_CANT_WITH_ACCT_GROUP_MEMBERSHPS"},
+ {8494, L"ERROR_DS_NC_MUST_HAVE_NC_PARENT"},
+ {8495, L"ERROR_DS_CR_IMPOSSIBLE_TO_VALIDATE"},
+ {8496, L"ERROR_DS_DST_DOMAIN_NOT_NATIVE"},
+ {8497, L"ERROR_DS_MISSING_INFRASTRUCTURE_CONTAINER"},
+ {8498, L"ERROR_DS_CANT_MOVE_ACCOUNT_GROUP"},
+ {8499, L"ERROR_DS_CANT_MOVE_RESOURCE_GROUP"},
+ {8500, L"ERROR_DS_INVALID_SEARCH_FLAG"},
+ {8501, L"ERROR_DS_NO_TREE_DELETE_ABOVE_NC"},
+ {8502, L"ERROR_DS_COULDNT_LOCK_TREE_FOR_DELETE"},
+ {8503, L"ERROR_DS_COULDNT_IDENTIFY_OBJECTS_FOR_TREE_DELETE"},
+ {8504, L"ERROR_DS_SAM_INIT_FAILURE"},
+ {8505, L"ERROR_DS_SENSITIVE_GROUP_VIOLATION"},
+ {8506, L"ERROR_DS_CANT_MOD_PRIMARYGROUPID"},
+ {8507, L"ERROR_DS_ILLEGAL_BASE_SCHEMA_MOD"},
+ {8508, L"ERROR_DS_NONSAFE_SCHEMA_CHANGE"},
+ {8509, L"ERROR_DS_SCHEMA_UPDATE_DISALLOWED"},
+ {8510, L"ERROR_DS_CANT_CREATE_UNDER_SCHEMA"},
+ {8511, L"ERROR_DS_INSTALL_NO_SRC_SCH_VERSION"},
+ {8512, L"ERROR_DS_INSTALL_NO_SCH_VERSION_IN_INIFILE"},
+ {8513, L"ERROR_DS_INVALID_GROUP_TYPE"},
+ {8514, L"ERROR_DS_NO_NEST_GLOBALGROUP_IN_MIXEDDOMAIN"},
+ {8515, L"ERROR_DS_NO_NEST_LOCALGROUP_IN_MIXEDDOMAIN"},
+ {8516, L"ERROR_DS_GLOBAL_CANT_HAVE_LOCAL_MEMBER"},
+ {8517, L"ERROR_DS_GLOBAL_CANT_HAVE_UNIVERSAL_MEMBER"},
+ {8518, L"ERROR_DS_UNIVERSAL_CANT_HAVE_LOCAL_MEMBER"},
+ {8519, L"ERROR_DS_GLOBAL_CANT_HAVE_CROSSDOMAIN_MEMBER"},
+ {8520, L"ERROR_DS_LOCAL_CANT_HAVE_CROSSDOMAIN_LOCAL_MEMBER"},
+ {8521, L"ERROR_DS_HAVE_PRIMARY_MEMBERS"},
+ {8522, L"ERROR_DS_STRING_SD_CONVERSION_FAILED"},
+ {8523, L"ERROR_DS_NAMING_MASTER_GC"},
+ {8524, L"ERROR_DS_DNS_LOOKUP_FAILURE"},
+ {8525, L"ERROR_DS_COULDNT_UPDATE_SPNS"},
+ {8526, L"ERROR_DS_CANT_RETRIEVE_SD"},
+ {8527, L"ERROR_DS_KEY_NOT_UNIQUE"},
+ {8528, L"ERROR_DS_WRONG_LINKED_ATT_SYNTAX"},
+ {8529, L"ERROR_DS_SAM_NEED_BOOTKEY_PASSWORD"},
+ {8530, L"ERROR_DS_SAM_NEED_BOOTKEY_FLOPPY"},
+ {8531, L"ERROR_DS_CANT_START"},
+ {8532, L"ERROR_DS_INIT_FAILURE"},
+ {8533, L"ERROR_DS_NO_PKT_PRIVACY_ON_CONNECTION"},
+ {8534, L"ERROR_DS_SOURCE_DOMAIN_IN_FOREST"},
+ {8535, L"ERROR_DS_DESTINATION_DOMAIN_NOT_IN_FOREST"},
+ {8536, L"ERROR_DS_DESTINATION_AUDITING_NOT_ENABLED"},
+ {8537, L"ERROR_DS_CANT_FIND_DC_FOR_SRC_DOMAIN"},
+ {8538, L"ERROR_DS_SRC_OBJ_NOT_GROUP_OR_USER"},
+ {8539, L"ERROR_DS_SRC_SID_EXISTS_IN_FOREST"},
+ {8540, L"ERROR_DS_SRC_AND_DST_OBJECT_CLASS_MISMATCH"},
+ {8541, L"ERROR_SAM_INIT_FAILURE"},
+ {8542, L"ERROR_DS_DRA_SCHEMA_INFO_SHIP"},
+ {8543, L"ERROR_DS_DRA_SCHEMA_CONFLICT"},
+ {8544, L"ERROR_DS_DRA_EARLIER_SCHEMA_CONFLICT"},
+ {8545, L"ERROR_DS_DRA_OBJ_NC_MISMATCH"},
+ {8546, L"ERROR_DS_NC_STILL_HAS_DSAS"},
+ {8547, L"ERROR_DS_GC_REQUIRED"},
+ {8548, L"ERROR_DS_LOCAL_MEMBER_OF_LOCAL_ONLY"},
+ {8549, L"ERROR_DS_NO_FPO_IN_UNIVERSAL_GROUPS"},
+ {8550, L"ERROR_DS_CANT_ADD_TO_GC"},
+ {8551, L"ERROR_DS_NO_CHECKPOINT_WITH_PDC"},
+ {8552, L"ERROR_DS_SOURCE_AUDITING_NOT_ENABLED"},
+ {8553, L"ERROR_DS_CANT_CREATE_IN_NONDOMAIN_NC"},
+ {8554, L"ERROR_DS_INVALID_NAME_FOR_SPN"},
+ {8555, L"ERROR_DS_FILTER_USES_CONTRUCTED_ATTRS"},
+ {8556, L"ERROR_DS_UNICODEPWD_NOT_IN_QUOTES"},
+ {8557, L"ERROR_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED"},
+ {8558, L"ERROR_DS_MUST_BE_RUN_ON_DST_DC"},
+ {8559, L"ERROR_DS_SRC_DC_MUST_BE_SP4_OR_GREATER"},
+ {8560, L"ERROR_DS_CANT_TREE_DELETE_CRITICAL_OBJ"},
+ {8561, L"ERROR_DS_INIT_FAILURE_CONSOLE"},
+ {8562, L"ERROR_DS_SAM_INIT_FAILURE_CONSOLE"},
+ {8563, L"ERROR_DS_FOREST_VERSION_TOO_HIGH"},
+ {8564, L"ERROR_DS_DOMAIN_VERSION_TOO_HIGH"},
+ {8565, L"ERROR_DS_FOREST_VERSION_TOO_LOW"},
+ {8566, L"ERROR_DS_DOMAIN_VERSION_TOO_LOW"},
+ {8567, L"ERROR_DS_INCOMPATIBLE_VERSION"},
+ {8568, L"ERROR_DS_LOW_DSA_VERSION"},
+ {8569, L"ERROR_DS_NO_BEHAVIOR_VERSION_IN_MIXEDDOMAIN"},
+ {8570, L"ERROR_DS_NOT_SUPPORTED_SORT_ORDER"},
+ {8571, L"ERROR_DS_NAME_NOT_UNIQUE"},
+ {8572, L"ERROR_DS_MACHINE_ACCOUNT_CREATED_PRENT4"},
+ {8573, L"ERROR_DS_OUT_OF_VERSION_STORE"},
+ {8574, L"ERROR_DS_INCOMPATIBLE_CONTROLS_USED"},
+ {8575, L"ERROR_DS_NO_REF_DOMAIN"},
+ {8576, L"ERROR_DS_RESERVED_LINK_ID"},
+ {8577, L"ERROR_DS_LINK_ID_NOT_AVAILABLE"},
+ {8578, L"ERROR_DS_AG_CANT_HAVE_UNIVERSAL_MEMBER"},
+ {8579, L"ERROR_DS_MODIFYDN_DISALLOWED_BY_INSTANCE_TYPE"},
+ {8580, L"ERROR_DS_NO_OBJECT_MOVE_IN_SCHEMA_NC"},
+ {8581, L"ERROR_DS_MODIFYDN_DISALLOWED_BY_FLAG"},
+ {8582, L"ERROR_DS_MODIFYDN_WRONG_GRANDPARENT"},
+ {8583, L"ERROR_DS_NAME_ERROR_TRUST_REFERRAL"},
+ {8584, L"ERROR_NOT_SUPPORTED_ON_STANDARD_SERVER"},
+ {8585, L"ERROR_DS_CANT_ACCESS_REMOTE_PART_OF_AD"},
+ {8586, L"ERROR_DS_CR_IMPOSSIBLE_TO_VALIDATE_V2"},
+ {8587, L"ERROR_DS_THREAD_LIMIT_EXCEEDED"},
+ {8588, L"ERROR_DS_NOT_CLOSEST"},
+ {8589, L"ERROR_DS_CANT_DERIVE_SPN_WITHOUT_SERVER_REF"},
+ {8590, L"ERROR_DS_SINGLE_USER_MODE_FAILED"},
+ {8591, L"ERROR_DS_NTDSCRIPT_SYNTAX_ERROR"},
+ {8592, L"ERROR_DS_NTDSCRIPT_PROCESS_ERROR"},
+ {8593, L"ERROR_DS_DIFFERENT_REPL_EPOCHS"},
+ {8594, L"ERROR_DS_DRS_EXTENSIONS_CHANGED"},
+ {8595, L"ERROR_DS_REPLICA_SET_CHANGE_NOT_ALLOWED_ON_DISABLED_CR"},
+ {8596, L"ERROR_DS_NO_MSDS_INTID"},
+ {8597, L"ERROR_DS_DUP_MSDS_INTID"},
+ {8598, L"ERROR_DS_EXISTS_IN_RDNATTID"},
+ {8599, L"ERROR_DS_AUTHORIZATION_FAILED"},
+ {8600, L"ERROR_DS_INVALID_SCRIPT"},
+ {8601, L"ERROR_DS_REMOTE_CROSSREF_OP_FAILED"},
+ {8602, L"ERROR_DS_CROSS_REF_BUSY"},
+ {8603, L"ERROR_DS_CANT_DERIVE_SPN_FOR_DELETED_DOMAIN"},
+ {8604, L"ERROR_DS_CANT_DEMOTE_WITH_WRITEABLE_NC"},
+ {8605, L"ERROR_DS_DUPLICATE_ID_FOUND"},
+ {8606, L"ERROR_DS_INSUFFICIENT_ATTR_TO_CREATE_OBJECT"},
+ {8607, L"ERROR_DS_GROUP_CONVERSION_ERROR"},
+ {8608, L"ERROR_DS_CANT_MOVE_APP_BASIC_GROUP"},
+ {8609, L"ERROR_DS_CANT_MOVE_APP_QUERY_GROUP"},
+ {8610, L"ERROR_DS_ROLE_NOT_VERIFIED"},
+ {8611, L"ERROR_DS_WKO_CONTAINER_CANNOT_BE_SPECIAL"},
+ {8612, L"ERROR_DS_DOMAIN_RENAME_IN_PROGRESS"},
+ {8613, L"ERROR_DS_EXISTING_AD_CHILD_NC"},
+ {8614, L"ERROR_DS_REPL_LIFETIME_EXCEEDED"},
+ {8615, L"ERROR_DS_DISALLOWED_IN_SYSTEM_CONTAINER"},
+ {8616, L"ERROR_DS_LDAP_SEND_QUEUE_FULL"},
+ {8617, L"ERROR_DS_DRA_OUT_SCHEDULE_WINDOW"},
+ {8618, L"ERROR_DS_POLICY_NOT_KNOWN"},
+ {8619, L"ERROR_NO_SITE_SETTINGS_OBJECT"},
+ {8620, L"ERROR_NO_SECRETS"},
+ {8621, L"ERROR_NO_WRITABLE_DC_FOUND"},
+ {8622, L"ERROR_DS_NO_SERVER_OBJECT"},
+ {8623, L"ERROR_DS_NO_NTDSA_OBJECT"},
+ {8624, L"ERROR_DS_NON_ASQ_SEARCH"},
+ {8625, L"ERROR_DS_AUDIT_FAILURE"},
+ {8626, L"ERROR_DS_INVALID_SEARCH_FLAG_SUBTREE"},
+ {8627, L"ERROR_DS_INVALID_SEARCH_FLAG_TUPLE"},
+ {8628, L"ERROR_DS_HIERARCHY_TABLE_TOO_DEEP"},
+ {8629, L"ERROR_DS_DRA_CORRUPT_UTD_VECTOR"},
+ {8630, L"ERROR_DS_DRA_SECRETS_DENIED"},
+ {8631, L"ERROR_DS_RESERVED_MAPI_ID"},
+ {8632, L"ERROR_DS_MAPI_ID_NOT_AVAILABLE"},
+ {8633, L"ERROR_DS_DRA_MISSING_KRBTGT_SECRET"},
+ {8634, L"ERROR_DS_DOMAIN_NAME_EXISTS_IN_FOREST"},
+ {8635, L"ERROR_DS_FLAT_NAME_EXISTS_IN_FOREST"},
+ {8636, L"ERROR_INVALID_USER_PRINCIPAL_NAME"},
+ {8637, L"ERROR_DS_OID_MAPPED_GROUP_CANT_HAVE_MEMBERS"},
+ {8638, L"ERROR_DS_OID_NOT_FOUND"},
+ {8639, L"ERROR_DS_DRA_RECYCLED_TARGET"},
+ {8640, L"ERROR_DS_DISALLOWED_NC_REDIRECT"},
+ {8641, L"ERROR_DS_HIGH_ADLDS_FFL"},
+ {8642, L"ERROR_DS_HIGH_DSA_VERSION"},
+ {8643, L"ERROR_DS_LOW_ADLDS_FFL"},
+ {8644, L"ERROR_DOMAIN_SID_SAME_AS_LOCAL_WORKSTATION"},
+ {8645, L"ERROR_DS_UNDELETE_SAM_VALIDATION_FAILED"},
+ {8646, L"ERROR_INCORRECT_ACCOUNT_TYPE"},
+ {9001, L"DNS_ERROR_RCODE_FORMAT_ERROR"},
+ {9002, L"DNS_ERROR_RCODE_SERVER_FAILURE"},
+ {9003, L"DNS_ERROR_RCODE_NAME_ERROR"},
+ {9004, L"DNS_ERROR_RCODE_NOT_IMPLEMENTED"},
+ {9005, L"DNS_ERROR_RCODE_REFUSED"},
+ {9006, L"DNS_ERROR_RCODE_YXDOMAIN"},
+ {9007, L"DNS_ERROR_RCODE_YXRRSET"},
+ {9008, L"DNS_ERROR_RCODE_NXRRSET"},
+ {9009, L"DNS_ERROR_RCODE_NOTAUTH"},
+ {9010, L"DNS_ERROR_RCODE_NOTZONE"},
+ {9016, L"DNS_ERROR_RCODE_BADSIG"},
+ {9017, L"DNS_ERROR_RCODE_BADKEY"},
+ {9018, L"DNS_ERROR_RCODE_BADTIME"},
+ {9101, L"DNS_ERROR_KEYMASTER_REQUIRED"},
+ {9102, L"DNS_ERROR_NOT_ALLOWED_ON_SIGNED_ZONE"},
+ {9103, L"DNS_ERROR_NSEC3_INCOMPATIBLE_WITH_RSA_SHA1"},
+ {9104, L"DNS_ERROR_NOT_ENOUGH_SIGNING_KEY_DESCRIPTORS"},
+ {9105, L"DNS_ERROR_UNSUPPORTED_ALGORITHM"},
+ {9106, L"DNS_ERROR_INVALID_KEY_SIZE"},
+ {9107, L"DNS_ERROR_SIGNING_KEY_NOT_ACCESSIBLE"},
+ {9108, L"DNS_ERROR_KSP_DOES_NOT_SUPPORT_PROTECTION"},
+ {9109, L"DNS_ERROR_UNEXPECTED_DATA_PROTECTION_ERROR"},
+ {9110, L"DNS_ERROR_UNEXPECTED_CNG_ERROR"},
+ {9111, L"DNS_ERROR_UNKNOWN_SIGNING_PARAMETER_VERSION"},
+ {9112, L"DNS_ERROR_KSP_NOT_ACCESSIBLE"},
+ {9113, L"DNS_ERROR_TOO_MANY_SKDS"},
+ {9114, L"DNS_ERROR_INVALID_ROLLOVER_PERIOD"},
+ {9115, L"DNS_ERROR_INVALID_INITIAL_ROLLOVER_OFFSET"},
+ {9116, L"DNS_ERROR_ROLLOVER_IN_PROGRESS"},
+ {9117, L"DNS_ERROR_STANDBY_KEY_NOT_PRESENT"},
+ {9118, L"DNS_ERROR_NOT_ALLOWED_ON_ZSK"},
+ {9119, L"DNS_ERROR_NOT_ALLOWED_ON_ACTIVE_SKD"},
+ {9120, L"DNS_ERROR_ROLLOVER_ALREADY_QUEUED"},
+ {9121, L"DNS_ERROR_NOT_ALLOWED_ON_UNSIGNED_ZONE"},
+ {9122, L"DNS_ERROR_BAD_KEYMASTER"},
+ {9123, L"DNS_ERROR_INVALID_SIGNATURE_VALIDITY_PERIOD"},
+ {9124, L"DNS_ERROR_INVALID_NSEC3_ITERATION_COUNT"},
+ {9125, L"DNS_ERROR_DNSSEC_IS_DISABLED"},
+ {9126, L"DNS_ERROR_INVALID_XML"},
+ {9127, L"DNS_ERROR_NO_VALID_TRUST_ANCHORS"},
+ {9128, L"DNS_ERROR_ROLLOVER_NOT_POKEABLE"},
+ {9129, L"DNS_ERROR_NSEC3_NAME_COLLISION"},
+ {9130, L"DNS_ERROR_NSEC_INCOMPATIBLE_WITH_NSEC3_RSA_SHA1"},
+ {9501, L"DNS_INFO_NO_RECORDS"},
+ {9502, L"DNS_ERROR_BAD_PACKET"},
+ {9503, L"DNS_ERROR_NO_PACKET"},
+ {9504, L"DNS_ERROR_RCODE"},
+ {9505, L"DNS_ERROR_UNSECURE_PACKET"},
+ {9506, L"DNS_REQUEST_PENDING"},
+ {9551, L"DNS_ERROR_INVALID_TYPE"},
+ {9552, L"DNS_ERROR_INVALID_IP_ADDRESS"},
+ {9553, L"DNS_ERROR_INVALID_PROPERTY"},
+ {9554, L"DNS_ERROR_TRY_AGAIN_LATER"},
+ {9555, L"DNS_ERROR_NOT_UNIQUE"},
+ {9556, L"DNS_ERROR_NON_RFC_NAME"},
+ {9557, L"DNS_STATUS_FQDN"},
+ {9558, L"DNS_STATUS_DOTTED_NAME"},
+ {9559, L"DNS_STATUS_SINGLE_PART_NAME"},
+ {9560, L"DNS_ERROR_INVALID_NAME_CHAR"},
+ {9561, L"DNS_ERROR_NUMERIC_NAME"},
+ {9562, L"DNS_ERROR_NOT_ALLOWED_ON_ROOT_SERVER"},
+ {9563, L"DNS_ERROR_NOT_ALLOWED_UNDER_DELEGATION"},
+ {9564, L"DNS_ERROR_CANNOT_FIND_ROOT_HINTS"},
+ {9565, L"DNS_ERROR_INCONSISTENT_ROOT_HINTS"},
+ {9566, L"DNS_ERROR_DWORD_VALUE_TOO_SMALL"},
+ {9567, L"DNS_ERROR_DWORD_VALUE_TOO_LARGE"},
+ {9568, L"DNS_ERROR_BACKGROUND_LOADING"},
+ {9569, L"DNS_ERROR_NOT_ALLOWED_ON_RODC"},
+ {9570, L"DNS_ERROR_NOT_ALLOWED_UNDER_DNAME"},
+ {9571, L"DNS_ERROR_DELEGATION_REQUIRED"},
+ {9572, L"DNS_ERROR_INVALID_POLICY_TABLE"},
+ {9601, L"DNS_ERROR_ZONE_DOES_NOT_EXIST"},
+ {9602, L"DNS_ERROR_NO_ZONE_INFO"},
+ {9603, L"DNS_ERROR_INVALID_ZONE_OPERATION"},
+ {9604, L"DNS_ERROR_ZONE_CONFIGURATION_ERROR"},
+ {9605, L"DNS_ERROR_ZONE_HAS_NO_SOA_RECORD"},
+ {9606, L"DNS_ERROR_ZONE_HAS_NO_NS_RECORDS"},
+ {9607, L"DNS_ERROR_ZONE_LOCKED"},
+ {9608, L"DNS_ERROR_ZONE_CREATION_FAILED"},
+ {9609, L"DNS_ERROR_ZONE_ALREADY_EXISTS"},
+ {9610, L"DNS_ERROR_AUTOZONE_ALREADY_EXISTS"},
+ {9611, L"DNS_ERROR_INVALID_ZONE_TYPE"},
+ {9612, L"DNS_ERROR_SECONDARY_REQUIRES_MASTER_IP"},
+ {9613, L"DNS_ERROR_ZONE_NOT_SECONDARY"},
+ {9614, L"DNS_ERROR_NEED_SECONDARY_ADDRESSES"},
+ {9615, L"DNS_ERROR_WINS_INIT_FAILED"},
+ {9616, L"DNS_ERROR_NEED_WINS_SERVERS"},
+ {9617, L"DNS_ERROR_NBSTAT_INIT_FAILED"},
+ {9618, L"DNS_ERROR_SOA_DELETE_INVALID"},
+ {9619, L"DNS_ERROR_FORWARDER_ALREADY_EXISTS"},
+ {9620, L"DNS_ERROR_ZONE_REQUIRES_MASTER_IP"},
+ {9621, L"DNS_ERROR_ZONE_IS_SHUTDOWN"},
+ {9622, L"DNS_ERROR_ZONE_LOCKED_FOR_SIGNING"},
+ {9651, L"DNS_ERROR_PRIMARY_REQUIRES_DATAFILE"},
+ {9652, L"DNS_ERROR_INVALID_DATAFILE_NAME"},
+ {9653, L"DNS_ERROR_DATAFILE_OPEN_FAILURE"},
+ {9654, L"DNS_ERROR_FILE_WRITEBACK_FAILED"},
+ {9655, L"DNS_ERROR_DATAFILE_PARSING"},
+ {9701, L"DNS_ERROR_RECORD_DOES_NOT_EXIST"},
+ {9702, L"DNS_ERROR_RECORD_FORMAT"},
+ {9703, L"DNS_ERROR_NODE_CREATION_FAILED"},
+ {9704, L"DNS_ERROR_UNKNOWN_RECORD_TYPE"},
+ {9705, L"DNS_ERROR_RECORD_TIMED_OUT"},
+ {9706, L"DNS_ERROR_NAME_NOT_IN_ZONE"},
+ {9707, L"DNS_ERROR_CNAME_LOOP"},
+ {9708, L"DNS_ERROR_NODE_IS_CNAME"},
+ {9709, L"DNS_ERROR_CNAME_COLLISION"},
+ {9710, L"DNS_ERROR_RECORD_ONLY_AT_ZONE_ROOT"},
+ {9711, L"DNS_ERROR_RECORD_ALREADY_EXISTS"},
+ {9712, L"DNS_ERROR_SECONDARY_DATA"},
+ {9713, L"DNS_ERROR_NO_CREATE_CACHE_DATA"},
+ {9714, L"DNS_ERROR_NAME_DOES_NOT_EXIST"},
+ {9715, L"DNS_WARNING_PTR_CREATE_FAILED"},
+ {9716, L"DNS_WARNING_DOMAIN_UNDELETED"},
+ {9717, L"DNS_ERROR_DS_UNAVAILABLE"},
+ {9718, L"DNS_ERROR_DS_ZONE_ALREADY_EXISTS"},
+ {9719, L"DNS_ERROR_NO_BOOTFILE_IF_DS_ZONE"},
+ {9720, L"DNS_ERROR_NODE_IS_DNAME"},
+ {9721, L"DNS_ERROR_DNAME_COLLISION"},
+ {9722, L"DNS_ERROR_ALIAS_LOOP"},
+ {9751, L"DNS_INFO_AXFR_COMPLETE"},
+ {9752, L"DNS_ERROR_AXFR"},
+ {9753, L"DNS_INFO_ADDED_LOCAL_WINS"},
+ {9801, L"DNS_STATUS_CONTINUE_NEEDED"},
+ {9851, L"DNS_ERROR_NO_TCPIP"},
+ {9852, L"DNS_ERROR_NO_DNS_SERVERS"},
+ {9901, L"DNS_ERROR_DP_DOES_NOT_EXIST"},
+ {9902, L"DNS_ERROR_DP_ALREADY_EXISTS"},
+ {9903, L"DNS_ERROR_DP_NOT_ENLISTED"},
+ {9904, L"DNS_ERROR_DP_ALREADY_ENLISTED"},
+ {9905, L"DNS_ERROR_DP_NOT_AVAILABLE"},
+ {9906, L"DNS_ERROR_DP_FSMO_ERROR"},
+ {10004, L"WSAEINTR"},
+ {10009, L"WSAEBADF"},
+ {10013, L"WSAEACCES"},
+ {10014, L"WSAEFAULT"},
+ {10022, L"WSAEINVAL"},
+ {10024, L"WSAEMFILE"},
+ {10035, L"WSAEWOULDBLOCK"},
+ {10036, L"WSAEINPROGRESS"},
+ {10037, L"WSAEALREADY"},
+ {10038, L"WSAENOTSOCK"},
+ {10039, L"WSAEDESTADDRREQ"},
+ {10040, L"WSAEMSGSIZE"},
+ {10041, L"WSAEPROTOTYPE"},
+ {10042, L"WSAENOPROTOOPT"},
+ {10043, L"WSAEPROTONOSUPPORT"},
+ {10044, L"WSAESOCKTNOSUPPORT"},
+ {10045, L"WSAEOPNOTSUPP"},
+ {10046, L"WSAEPFNOSUPPORT"},
+ {10047, L"WSAEAFNOSUPPORT"},
+ {10048, L"WSAEADDRINUSE"},
+ {10049, L"WSAEADDRNOTAVAIL"},
+ {10050, L"WSAENETDOWN"},
+ {10051, L"WSAENETUNREACH"},
+ {10052, L"WSAENETRESET"},
+ {10053, L"WSAECONNABORTED"},
+ {10054, L"WSAECONNRESET"},
+ {10055, L"WSAENOBUFS"},
+ {10056, L"WSAEISCONN"},
+ {10057, L"WSAENOTCONN"},
+ {10058, L"WSAESHUTDOWN"},
+ {10059, L"WSAETOOMANYREFS"},
+ {10060, L"WSAETIMEDOUT"},
+ {10061, L"WSAECONNREFUSED"},
+ {10062, L"WSAELOOP"},
+ {10063, L"WSAENAMETOOLONG"},
+ {10064, L"WSAEHOSTDOWN"},
+ {10065, L"WSAEHOSTUNREACH"},
+ {10066, L"WSAENOTEMPTY"},
+ {10067, L"WSAEPROCLIM"},
+ {10068, L"WSAEUSERS"},
+ {10069, L"WSAEDQUOT"},
+ {10070, L"WSAESTALE"},
+ {10071, L"WSAEREMOTE"},
+ {10091, L"WSASYSNOTREADY"},
+ {10092, L"WSAVERNOTSUPPORTED"},
+ {10093, L"WSANOTINITIALISED"},
+ {10101, L"WSAEDISCON"},
+ {10102, L"WSAENOMORE"},
+ {10103, L"WSAECANCELLED"},
+ {10104, L"WSAEINVALIDPROCTABLE"},
+ {10105, L"WSAEINVALIDPROVIDER"},
+ {10106, L"WSAEPROVIDERFAILEDINIT"},
+ {10107, L"WSASYSCALLFAILURE"},
+ {10108, L"WSASERVICE_NOT_FOUND"},
+ {10109, L"WSATYPE_NOT_FOUND"},
+ {10110, L"WSA_E_NO_MORE"},
+ {10111, L"WSA_E_CANCELLED"},
+ {10112, L"WSAEREFUSED"},
+ {11001, L"WSAHOST_NOT_FOUND"},
+ {11002, L"WSATRY_AGAIN"},
+ {11003, L"WSANO_RECOVERY"},
+ {11004, L"WSANO_DATA"},
+ {11005, L"WSA_QOS_RECEIVERS"},
+ {11006, L"WSA_QOS_SENDERS"},
+ {11007, L"WSA_QOS_NO_SENDERS"},
+ {11008, L"WSA_QOS_NO_RECEIVERS"},
+ {11009, L"WSA_QOS_REQUEST_CONFIRMED"},
+ {11010, L"WSA_QOS_ADMISSION_FAILURE"},
+ {11011, L"WSA_QOS_POLICY_FAILURE"},
+ {11012, L"WSA_QOS_BAD_STYLE"},
+ {11013, L"WSA_QOS_BAD_OBJECT"},
+ {11014, L"WSA_QOS_TRAFFIC_CTRL_ERROR"},
+ {11015, L"WSA_QOS_GENERIC_ERROR"},
+ {11016, L"WSA_QOS_ESERVICETYPE"},
+ {11017, L"WSA_QOS_EFLOWSPEC"},
+ {11018, L"WSA_QOS_EPROVSPECBUF"},
+ {11019, L"WSA_QOS_EFILTERSTYLE"},
+ {11020, L"WSA_QOS_EFILTERTYPE"},
+ {11021, L"WSA_QOS_EFILTERCOUNT"},
+ {11022, L"WSA_QOS_EOBJLENGTH"},
+ {11023, L"WSA_QOS_EFLOWCOUNT"},
+ {11024, L"WSA_QOS_EUNKOWNPSOBJ"},
+ {11025, L"WSA_QOS_EPOLICYOBJ"},
+ {11026, L"WSA_QOS_EFLOWDESC"},
+ {11027, L"WSA_QOS_EPSFLOWSPEC"},
+ {11028, L"WSA_QOS_EPSFILTERSPEC"},
+ {11029, L"WSA_QOS_ESDMODEOBJ"},
+ {11030, L"WSA_QOS_ESHAPERATEOBJ"},
+ {11031, L"WSA_QOS_RESERVED_PETYPE"},
+ {11032, L"WSA_SECURE_HOST_NOT_FOUND"},
+ {11033, L"WSA_IPSEC_NAME_POLICY_ERROR"}};
+
+const wchar_t* errorCodeName(DWORD code)
+{
+ static const wchar_t* NotFound = L"unknown error";
+
+ auto itor = std::lower_bound(std::begin(g_codes), std::end(g_codes), CodeName{code},
+ [](auto&& a, auto&& b) {
+ return (a.code < b.code);
+ });
+
+ if (itor != std::end(g_codes)) {
+ return itor->name;
+ }
+
+ return NotFound;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/eventfilter.cpp b/libs/uibase/src/eventfilter.cpp
new file mode 100644
index 0000000..3b624c7
--- /dev/null
+++ b/libs/uibase/src/eventfilter.cpp
@@ -0,0 +1,34 @@
+/*
+Copyright (C) 2016 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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/eventfilter.h>
+
+namespace MOBase
+{
+
+EventFilter::EventFilter(QObject* parent, const EventFilter::HandlerFunc& handler)
+ : QObject(parent), m_Handler(handler)
+{}
+
+bool EventFilter::eventFilter(QObject* obj, QEvent* event)
+{
+ return m_Handler(obj, event);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/executableinfo.cpp b/libs/uibase/src/executableinfo.cpp
new file mode 100644
index 0000000..1573a0b
--- /dev/null
+++ b/libs/uibase/src/executableinfo.cpp
@@ -0,0 +1,105 @@
+#include <uibase/executableinfo.h>
+
+using namespace MOBase;
+
+ExecutableForcedLoadSetting::ExecutableForcedLoadSetting(const QString& process,
+ const QString& library)
+ : m_Enabled(false), m_Process(process), m_Library(library), m_Forced(false)
+{}
+
+ExecutableForcedLoadSetting& ExecutableForcedLoadSetting::withEnabled(bool enabled)
+{
+ m_Enabled = enabled;
+ return *this;
+}
+
+ExecutableForcedLoadSetting& ExecutableForcedLoadSetting::withForced(bool forced)
+{
+ m_Forced = forced;
+ return *this;
+}
+
+bool ExecutableForcedLoadSetting::enabled() const
+{
+ return m_Enabled;
+}
+
+bool ExecutableForcedLoadSetting::forced() const
+{
+ return m_Forced;
+}
+
+QString ExecutableForcedLoadSetting::library() const
+{
+ return m_Library;
+}
+
+QString ExecutableForcedLoadSetting::process() const
+{
+ return m_Process;
+}
+
+MOBase::ExecutableInfo::ExecutableInfo(const QString& title, const QFileInfo& binary)
+ : m_Title(title), m_Binary(binary),
+ m_WorkingDirectory(binary.exists() ? binary.absoluteDir() : QString()),
+ m_SteamAppID()
+{}
+
+ExecutableInfo& ExecutableInfo::withArgument(const QString& argument)
+{
+ m_Arguments.append(argument);
+ return *this;
+}
+
+ExecutableInfo& ExecutableInfo::withWorkingDirectory(const QDir& workingDirectory)
+{
+ m_WorkingDirectory = workingDirectory;
+ return *this;
+}
+
+ExecutableInfo& MOBase::ExecutableInfo::withSteamAppId(const QString& appId)
+{
+ m_SteamAppID = appId;
+ return *this;
+}
+
+ExecutableInfo& ExecutableInfo::asCustom()
+{
+ m_Custom = true;
+ return *this;
+}
+
+bool ExecutableInfo::isValid() const
+{
+ return m_Binary.exists();
+}
+
+QString ExecutableInfo::title() const
+{
+ return m_Title;
+}
+
+QFileInfo ExecutableInfo::binary() const
+{
+ return m_Binary;
+}
+
+QStringList ExecutableInfo::arguments() const
+{
+ return m_Arguments;
+}
+
+QDir ExecutableInfo::workingDirectory() const
+{
+ return m_WorkingDirectory;
+}
+
+QString ExecutableInfo::steamAppID() const
+{
+ return m_SteamAppID;
+}
+
+bool ExecutableInfo::isCustom() const
+{
+ return m_Custom;
+}
diff --git a/libs/uibase/src/expanderwidget.cpp b/libs/uibase/src/expanderwidget.cpp
new file mode 100644
index 0000000..451ab07
--- /dev/null
+++ b/libs/uibase/src/expanderwidget.cpp
@@ -0,0 +1,93 @@
+#include <uibase/expanderwidget.h>
+#include <QIODevice>
+
+namespace MOBase
+{
+
+ExpanderWidget::ExpanderWidget() : m_button(nullptr), m_content(nullptr), opened_(false)
+{}
+
+ExpanderWidget::ExpanderWidget(QToolButton* button, QWidget* content) : ExpanderWidget()
+{
+ set(button, content);
+}
+
+void ExpanderWidget::set(QToolButton* button, QWidget* content, bool o)
+{
+ m_button = button;
+ m_content = content;
+
+ m_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ QObject::connect(m_button, &QToolButton::clicked, [&] {
+ toggle();
+ });
+
+ toggle(o);
+}
+
+void ExpanderWidget::toggle()
+{
+ if (opened()) {
+ toggle(false);
+ } else {
+ toggle(true);
+ }
+}
+
+void ExpanderWidget::toggle(bool b)
+{
+ if (b != opened_) {
+ emit aboutToToggle(b);
+ }
+
+ if (b) {
+ m_button->setArrowType(Qt::DownArrow);
+ m_content->show();
+ } else {
+ m_button->setArrowType(Qt::RightArrow);
+ m_content->hide();
+ }
+
+ if (b != opened_) {
+ // the state has to be remembered instead of using m_content's visibility
+ // because saving the state in saveConflictExpandersState() happens after the
+ // dialog is closed, which marks all the widgets hidden
+ opened_ = b;
+
+ emit toggled(b);
+ }
+}
+
+bool ExpanderWidget::opened() const
+{
+ return opened_;
+}
+
+QByteArray ExpanderWidget::saveState() const
+{
+ QByteArray result;
+ QDataStream stream(&result, QIODevice::WriteOnly);
+
+ stream << opened();
+
+ return result;
+}
+
+void ExpanderWidget::restoreState(const QByteArray& a)
+{
+ QDataStream stream(a);
+
+ bool opened = false;
+ stream >> opened;
+
+ if (stream.status() == QDataStream::Ok) {
+ toggle(opened);
+ }
+}
+
+QToolButton* ExpanderWidget::button() const
+{
+ return m_button;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/filesystemutilities.cpp b/libs/uibase/src/filesystemutilities.cpp
new file mode 100644
index 0000000..6a01324
--- /dev/null
+++ b/libs/uibase/src/filesystemutilities.cpp
@@ -0,0 +1,68 @@
+#include <uibase/filesystemutilities.h>
+
+#include <QRegularExpression>
+#include <QString>
+
+namespace MOBase
+{
+
+bool fixDirectoryName(QString& name)
+{
+ QString temp = name.simplified();
+ while (temp.endsWith('.'))
+ temp.chop(1);
+
+ temp.replace(QRegularExpression(R"([<>:"/\\|?*])"), "");
+ static QString invalidNames[] = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2",
+ "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
+ "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
+ "LPT6", "LPT7", "LPT8", "LPT9"};
+ for (unsigned int i = 0; i < sizeof(invalidNames) / sizeof(QString); ++i) {
+ if (temp == invalidNames[i]) {
+ temp = "";
+ break;
+ }
+ }
+
+ temp = temp.simplified();
+
+ if (temp.length() >= 1) {
+ name = temp;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+QString sanitizeFileName(const QString& name, const QString& replacement)
+{
+ QString new_name = name;
+
+ // Remove characters not allowed by Windows
+ new_name.replace(QRegularExpression("[\\x{00}-\\x{1f}\\\\/:\\*\\?\"<>|]"),
+ replacement);
+
+ // Don't end with a period or a space
+ // Don't be "." or ".."
+ new_name.remove(QRegularExpression("[\\. ]*$"));
+
+ // Recurse until stuff stops changing
+ if (new_name != name) {
+ return sanitizeFileName(new_name);
+ }
+ return new_name;
+}
+
+bool validFileName(const QString& name)
+{
+ if (name.isEmpty()) {
+ return false;
+ }
+ if (name == "." || name == "..") {
+ return false;
+ }
+
+ return (name == sanitizeFileName(name));
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/filterwidget.cpp b/libs/uibase/src/filterwidget.cpp
new file mode 100644
index 0000000..cb0c503
--- /dev/null
+++ b/libs/uibase/src/filterwidget.cpp
@@ -0,0 +1,693 @@
+#include <uibase/filterwidget.h>
+#include <uibase/eventfilter.h>
+#include <uibase/log.h>
+#include <QApplication>
+#include <QContextMenuEvent>
+#include <QDialog>
+#include <QEvent>
+#include <QLineEdit>
+#include <QMenu>
+#include <QSortFilterProxyModel>
+#include <QTimer>
+
+namespace MOBase
+{
+
+void setStyleProperty(QWidget* w, const char* k, const QVariant& v)
+{
+ w->setProperty(k, v);
+
+ if (auto* s = w->style()) {
+ // changing properties doesn't repolish automatically
+ s->unpolish(w);
+ s->polish(w);
+ }
+
+ // the qss will probably change the border, which requires sending a manual
+ // StyleChange because the box model has changed
+ QEvent event(QEvent::StyleChange);
+ QApplication::sendEvent(w, &event);
+ w->update();
+ w->updateGeometry();
+}
+
+FilterWidgetProxyModel::FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent)
+ : QSortFilterProxyModel(parent), m_filter(fw)
+{
+ setRecursiveFilteringEnabled(true);
+}
+
+bool FilterWidgetProxyModel::filterAcceptsRow(int sourceRow,
+ const QModelIndex& sourceParent) const
+{
+ if (!m_filter.filteringEnabled()) {
+ return true;
+ }
+
+ const auto cols = sourceModel()->columnCount();
+
+ const auto m = m_filter.matches([&](auto&& regex) {
+ if (m_filter.filterColumn() == -1) {
+ for (int c = 0; c < cols; ++c) {
+ if (columnMatches(sourceRow, sourceParent, c, regex)) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ return columnMatches(sourceRow, sourceParent, m_filter.filterColumn(), regex);
+ }
+ });
+
+ return m;
+}
+
+bool FilterWidgetProxyModel::columnMatches(int sourceRow,
+ const QModelIndex& sourceParent, int c,
+ const QRegularExpression& regex) const
+{
+ QModelIndex index = sourceModel()->index(sourceRow, c, sourceParent);
+ const auto text = sourceModel()->data(index, Qt::DisplayRole).toString();
+
+ return regex.match(text).hasMatch();
+}
+
+void FilterWidgetProxyModel::sort(int column, Qt::SortOrder order)
+{
+ if (m_filter.useSourceSort()) {
+ // without this, the proxy seems to sometimes ignore the sorting done below,
+ // no idea why
+ QSortFilterProxyModel::sort(-1, order);
+
+ sourceModel()->sort(column, order);
+ } else {
+ QSortFilterProxyModel::sort(column, order);
+ }
+}
+
+bool FilterWidgetProxyModel::lessThan(const QModelIndex& left,
+ const QModelIndex& right) const
+{
+ if (auto lt = m_filter.sortPredicate()) {
+ return lt(left, right);
+ } else {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+}
+
+static FilterWidget::Options s_options;
+
+FilterWidget::FilterWidget()
+ : m_edit(nullptr), m_list(nullptr), m_proxy(nullptr), m_eventFilter(nullptr),
+ m_clear(nullptr), m_timer(nullptr), m_useDelay(false), m_valid(true),
+ m_useSourceSort(false), m_filterColumn(-1), m_filteringEnabled(true),
+ m_filteredBorder(true)
+{
+ m_timer = new QTimer(this);
+ m_timer->setSingleShot(true);
+ QObject::connect(
+ m_timer, &QTimer::timeout, this,
+ [&] {
+ set();
+ },
+ Qt::QueuedConnection);
+}
+
+void FilterWidget::setOptions(const Options& o)
+{
+ s_options = o;
+}
+
+FilterWidget::Options FilterWidget::options()
+{
+ return s_options;
+}
+
+void FilterWidget::setEdit(QLineEdit* edit)
+{
+ unhookEdit();
+
+ m_edit = edit;
+
+ if (m_edit) {
+ m_edit->setPlaceholderText(QObject::tr("Filter"));
+
+ createClear();
+ hookEdit();
+ clear();
+ }
+}
+
+void FilterWidget::setList(QAbstractItemView* list)
+{
+ unhookList();
+
+ m_list = list;
+
+ if (list) {
+ hookList();
+ } else {
+ m_proxy = nullptr;
+ }
+}
+
+void FilterWidget::clear()
+{
+ if (!m_edit) {
+ return;
+ }
+
+ m_edit->clear();
+}
+
+void FilterWidget::scrollToSelection()
+{
+ if (options().scrollToSelection && m_list &&
+ m_list->selectionModel()->hasSelection()) {
+ m_list->scrollTo(m_list->selectionModel()->selectedIndexes()[0]);
+ }
+}
+
+bool FilterWidget::empty() const
+{
+ return m_text.isEmpty();
+}
+
+void FilterWidget::setUpdateDelay(bool b)
+{
+ m_useDelay = b;
+}
+
+bool FilterWidget::hasUpdateDelay() const
+{
+ return m_useDelay;
+}
+
+void FilterWidget::setUseSourceSort(bool b)
+{
+ m_useSourceSort = b;
+}
+
+bool FilterWidget::useSourceSort() const
+{
+ return m_useSourceSort;
+}
+
+void FilterWidget::setSortPredicate(sortFun f)
+{
+ m_lt = f;
+}
+
+const FilterWidget::sortFun& FilterWidget::sortPredicate() const
+{
+ return m_lt;
+}
+
+void FilterWidget::setFilterColumn(int i)
+{
+ m_filterColumn = i;
+}
+
+int FilterWidget::filterColumn() const
+{
+ return m_filterColumn;
+}
+
+void FilterWidget::setFilteringEnabled(bool b)
+{
+ m_filteringEnabled = b;
+}
+
+bool FilterWidget::filteringEnabled() const
+{
+ return m_filteringEnabled;
+}
+
+void FilterWidget::setFilteredBorder(bool b)
+{
+ m_filteredBorder = b;
+}
+
+bool FilterWidget::filteredBorder() const
+{
+ return m_filteredBorder;
+}
+
+FilterWidgetProxyModel* FilterWidget::proxyModel()
+{
+ return m_proxy;
+}
+
+QAbstractItemModel* FilterWidget::sourceModel()
+{
+ if (m_proxy) {
+ return m_proxy->sourceModel();
+ } else if (m_list) {
+ return m_list->model();
+ } else {
+ return nullptr;
+ }
+}
+
+QModelIndex FilterWidget::mapFromSource(const QModelIndex& index) const
+{
+ if (m_proxy) {
+ return m_proxy->mapFromSource(index);
+ } else {
+ log::error("FilterWidget::mapFromSource() called, but proxy isn't set up");
+ return index;
+ }
+}
+
+QModelIndex FilterWidget::mapToSource(const QModelIndex& index) const
+{
+ if (m_proxy) {
+ return m_proxy->mapToSource(index);
+ } else {
+ log::error("FilterWidget::mapToSource() called, but proxy isn't set up");
+ return index;
+ }
+}
+
+QItemSelection FilterWidget::mapSelectionFromSource(const QItemSelection& sel) const
+{
+ if (m_proxy) {
+ return m_proxy->mapSelectionFromSource(sel);
+ } else {
+ log::error("FilterWidget::mapToSource() called, but proxy isn't set up");
+ return sel;
+ }
+}
+
+QItemSelection FilterWidget::mapSelectionToSource(const QItemSelection& sel) const
+{
+ if (m_proxy) {
+ return m_proxy->mapSelectionToSource(sel);
+ } else {
+ log::error("FilterWidget::mapToSource() called, but proxy isn't set up");
+ return sel;
+ }
+}
+
+void FilterWidget::compile()
+{
+ Compiled compiled;
+
+ if (s_options.useRegex) {
+ QRegularExpression::PatternOptions flags =
+ QRegularExpression::DotMatchesEverythingOption;
+
+ if (!s_options.regexCaseSensitive) {
+ flags |= QRegularExpression::CaseInsensitiveOption;
+ }
+
+ if (s_options.regexExtended) {
+ flags |= QRegularExpression::ExtendedPatternSyntaxOption;
+ }
+
+ compiled.push_back({QRegularExpression(m_text, flags)});
+ } else {
+ const QStringList ORList = [&] {
+ QString filterCopy = QString(m_text);
+ filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
+ return filterCopy.split(";", Qt::SkipEmptyParts);
+ }();
+
+ // split in ORSegments that internally use AND logic
+ for (const auto& ORSegment : ORList) {
+ const auto keywords = ORSegment.split(" ", Qt::SkipEmptyParts);
+ QList<QRegularExpression> regexes;
+
+ for (const auto& keyword : keywords) {
+ const QString escaped = QRegularExpression::escape(keyword);
+
+ const auto flags = QRegularExpression::CaseInsensitiveOption |
+ QRegularExpression::DotMatchesEverythingOption;
+
+ regexes.push_back(QRegularExpression(escaped, flags));
+ }
+
+ compiled.push_back(regexes);
+ }
+ }
+
+ bool valid = true;
+
+ for (auto&& ANDKeywords : compiled) {
+ for (auto&& keyword : ANDKeywords) {
+ if (!keyword.isValid()) {
+ valid = false;
+ break;
+ }
+ }
+
+ if (!valid) {
+ break;
+ }
+ }
+
+ m_valid = valid;
+ setStyleProperty(m_edit, "valid-filter", m_valid);
+
+ if (m_valid) {
+ m_compiled = std::move(compiled);
+ }
+}
+
+bool FilterWidget::matches(predFun pred) const
+{
+ if (m_compiled.isEmpty() || !pred) {
+ return true;
+ }
+
+ for (auto& ANDKeywords : m_compiled) {
+ bool segmentGood = true;
+
+ // check each word in the segment for match, each word needs to be matched
+ // but it doesn't matter where.
+ for (auto& currentKeyword : ANDKeywords) {
+ if (!pred(currentKeyword)) {
+ segmentGood = false;
+ }
+ }
+
+ if (segmentGood) {
+ // the last AND loop didn't break so the ORSegments is true so mod
+ // matches filter
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool FilterWidget::matches(const QString& s) const
+{
+ return matches([&](const QRegularExpression& re) {
+ return re.match(s).hasMatch();
+ });
+}
+
+void FilterWidget::hookEdit()
+{
+ m_eventFilter = new EventFilter(m_edit, [&](auto* w, auto* e) {
+ if (e->type() == QEvent::Resize) {
+ onResized();
+ } else if (e->type() == QEvent::ContextMenu) {
+ onContextMenu(w, static_cast<QContextMenuEvent*>(e));
+ return true;
+ }
+
+ return false;
+ });
+
+ m_edit->installEventFilter(m_eventFilter);
+ QObject::connect(m_edit, &QLineEdit::textChanged, [&] {
+ onTextChanged();
+ });
+}
+
+void FilterWidget::unhookEdit()
+{
+ if (m_clear) {
+ delete m_clear;
+ m_clear = nullptr;
+ }
+
+ if (m_edit) {
+ m_edit->removeEventFilter(m_eventFilter);
+ }
+}
+
+void FilterWidget::hookList()
+{
+ m_proxy = new FilterWidgetProxyModel(*this);
+ m_proxy->setSourceModel(m_list->model());
+ m_list->setModel(m_proxy);
+
+ setShortcuts();
+}
+
+void FilterWidget::unhookList()
+{
+ if (m_proxy && m_list) {
+ auto* model = m_proxy->sourceModel();
+ m_proxy->setSourceModel(nullptr);
+ delete m_proxy;
+
+ m_list->setModel(model);
+ }
+
+ for (auto* s : m_shortcuts) {
+ delete s;
+ }
+
+ m_shortcuts.clear();
+}
+
+// walks up the parents of `w`, returns true if one of them is a QDialog
+//
+bool topLevelIsDialog(QWidget* w)
+{
+ if (!w) {
+ return false;
+ }
+
+ auto* p = w->parentWidget();
+ while (p) {
+ if (dynamic_cast<QDialog*>(p)) {
+ return true;
+ }
+
+ p = p->parentWidget();
+ }
+
+ return false;
+}
+
+void FilterWidget::setShortcuts()
+{
+ auto activate = [this] {
+ onFind();
+ };
+
+ auto reset = [this] {
+ onReset();
+ };
+
+ auto hookActivate = [&](auto* w) {
+ auto* s = new QShortcut(QKeySequence::Find, w);
+ s->setAutoRepeat(false);
+ s->setContext(Qt::WidgetWithChildrenShortcut);
+ QObject::connect(s, &QShortcut::activated, activate);
+ m_shortcuts.push_back(s);
+ };
+
+ auto hookReset = [&](auto* w) {
+ auto* s = new QShortcut(QKeySequence(Qt::Key_Escape), w);
+ s->setAutoRepeat(false);
+ s->setContext(Qt::WidgetWithChildrenShortcut);
+ QObject::connect(s, &QShortcut::activated, reset);
+ m_shortcuts.push_back(s);
+ };
+
+ // don't hook the escape key for reset when the filter is in a dialog, the
+ // standard behaviour is to close the dialog
+ const bool inDialog =
+ topLevelIsDialog(m_list ? static_cast<QWidget*>(m_list) : m_edit);
+
+ if (m_list) {
+ hookActivate(m_list);
+
+ if (!inDialog) {
+ hookReset(m_list);
+ }
+ }
+
+ if (m_edit) {
+ hookActivate(m_edit);
+
+ if (!inDialog) {
+ hookReset(m_edit);
+ }
+ }
+}
+
+void FilterWidget::onFind()
+{
+ if (m_edit) {
+ m_edit->setFocus();
+ m_edit->selectAll();
+ }
+}
+
+void FilterWidget::onReset()
+{
+ clear();
+
+ if (m_list) {
+ m_list->setFocus();
+ }
+}
+
+void FilterWidget::createClear()
+{
+ m_clear = new QToolButton(m_edit);
+
+ QPixmap pixmap(":/MO/gui/edit_clear");
+ m_clear->setIcon(QIcon(pixmap));
+ m_clear->setIconSize(pixmap.size());
+ m_clear->setCursor(Qt::ArrowCursor);
+ m_clear->setStyleSheet("QToolButton { border: none; padding: 0px; }");
+ m_clear->hide();
+
+ QObject::connect(m_clear, &QToolButton::clicked, [&] {
+ clear();
+ });
+
+ repositionClearButton();
+}
+
+void FilterWidget::set()
+{
+ const QString old = m_text;
+
+ QString currentText;
+ if (m_edit != nullptr) {
+ currentText = m_edit->text();
+ } else {
+ currentText = m_text;
+ }
+
+ emit aboutToChange(old, currentText);
+
+ m_text = currentText;
+ compile();
+
+ if (m_proxy) {
+ m_proxy->refreshFilter();
+ }
+
+ if (m_list) {
+ if (m_filteredBorder) {
+ setStyleProperty(m_list, "filtered", !m_text.isEmpty());
+ }
+
+ scrollToSelection();
+ }
+
+ emit changed(old, currentText);
+}
+
+void FilterWidget::update()
+{
+ if (!m_text.isEmpty()) {
+ set();
+ }
+}
+
+void FilterWidget::onTextChanged()
+{
+ m_clear->setVisible(!m_edit->text().isEmpty());
+
+ const auto text = m_edit->text();
+ if (text == m_text) {
+ return;
+ }
+
+ if (m_useDelay) {
+ m_timer->start(500);
+ } else {
+ set();
+ }
+}
+
+void FilterWidget::onResized()
+{
+ repositionClearButton();
+}
+
+void FilterWidget::onContextMenu(QObject*, QContextMenuEvent* e)
+{
+ std::unique_ptr<QMenu> m(m_edit->createStandardContextMenu());
+ m->setParent(m_edit);
+
+ auto* title = new QAction(tr("Filter options"), m_edit);
+ title->setEnabled(false);
+
+ auto f = title->font();
+ f.setBold(true);
+ title->setFont(f);
+
+ auto* regex = new QAction(tr("Use regular expressions"), m_edit);
+ regex->setStatusTip(tr("Use regular expressions in filters"));
+ regex->setCheckable(true);
+ regex->setChecked(s_options.useRegex);
+
+ connect(regex, &QAction::triggered, [&] {
+ s_options.useRegex = regex->isChecked();
+ update();
+ });
+
+ auto* cs = new QAction(tr("Case sensitive"), m_edit);
+ //: leave "(/i)" verbatim
+ cs->setStatusTip(tr("Make regular expressions case sensitive (/i)"));
+ cs->setCheckable(true);
+ cs->setChecked(s_options.regexCaseSensitive);
+ cs->setEnabled(s_options.useRegex);
+
+ connect(cs, &QAction::triggered, [&] {
+ s_options.regexCaseSensitive = cs->isChecked();
+ update();
+ });
+
+ auto* x = new QAction(tr("Extended"), m_edit);
+ //: leave "(/x)" verbatim
+ x->setStatusTip(tr("Ignores unescaped whitespace in regular expressions (/x)"));
+ x->setCheckable(true);
+ x->setChecked(s_options.regexExtended);
+ x->setEnabled(s_options.useRegex);
+
+ connect(x, &QAction::triggered, [&] {
+ s_options.regexExtended = x->isChecked();
+ update();
+ });
+
+ auto* sts = new QAction(tr("Keep selection in view"), m_edit);
+ sts->setStatusTip(tr("Scroll to keep the current selection in view after filtering"));
+ sts->setCheckable(true);
+ sts->setChecked(s_options.scrollToSelection);
+
+ connect(sts, &QAction::triggered, [&] {
+ s_options.scrollToSelection = sts->isChecked();
+ update();
+ });
+
+ m->insertSeparator(m->actions().first());
+ m->insertAction(m->actions().first(), x);
+ m->insertAction(m->actions().first(), cs);
+ m->insertAction(m->actions().first(), regex);
+ m->insertAction(m->actions().first(), sts);
+ m->insertAction(m->actions().first(), title);
+
+ m->exec(e->globalPos());
+}
+
+void FilterWidget::repositionClearButton()
+{
+ if (!m_clear) {
+ return;
+ }
+
+ const QSize sz = m_clear->sizeHint();
+ const int frame = m_edit->style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
+ const auto r = m_edit->rect();
+
+ const auto x = r.right() - frame - sz.width();
+ const auto y = (r.bottom() + 1 - sz.height()) / 2;
+
+ m_clear->move(x, y);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/finddialog.cpp b/libs/uibase/src/finddialog.cpp
new file mode 100644
index 0000000..9284014
--- /dev/null
+++ b/libs/uibase/src/finddialog.cpp
@@ -0,0 +1,51 @@
+/*
+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
+*/
+
+#include <uibase/finddialog.h>
+#include "ui_finddialog.h"
+
+namespace MOBase
+{
+
+FindDialog::FindDialog(QWidget* parent) : QDialog(parent), ui(new Ui::FindDialog)
+{
+ ui->setupUi(this);
+}
+
+FindDialog::~FindDialog()
+{
+ delete ui;
+}
+
+void FindDialog::on_nextBtn_clicked()
+{
+ emit findNext();
+}
+
+void FindDialog::on_patternEdit_textChanged(const QString& pattern)
+{
+ emit patternChanged(pattern);
+}
+
+void FindDialog::on_closeBtn_clicked()
+{
+ this->close();
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/finddialog.ui b/libs/uibase/src/finddialog.ui
new file mode 100644
index 0000000..b31145d
--- /dev/null
+++ b/libs/uibase/src/finddialog.ui
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FindDialog</class>
+ <widget class="QDialog" name="FindDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>72</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Find</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Find what:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="patternEdit">
+ <property name="toolTip">
+ <string>Search term</string>
+ </property>
+ <property name="whatsThis">
+ <string>Search term</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QPushButton" name="nextBtn">
+ <property name="toolTip">
+ <string>Find next occurence from current file position.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Find next occurence from current file position.</string>
+ </property>
+ <property name="text">
+ <string>&amp;Find Next</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeBtn">
+ <property name="toolTip">
+ <string>Close</string>
+ </property>
+ <property name="whatsThis">
+ <string>Close</string>
+ </property>
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/uibase/src/guessedvalue.cpp b/libs/uibase/src/guessedvalue.cpp
new file mode 100644
index 0000000..c8393af
--- /dev/null
+++ b/libs/uibase/src/guessedvalue.cpp
@@ -0,0 +1 @@
+#include <uibase/guessedvalue.h>
diff --git a/libs/uibase/src/ifiletree.cpp b/libs/uibase/src/ifiletree.cpp
new file mode 100644
index 0000000..b582d18
--- /dev/null
+++ b/libs/uibase/src/ifiletree.cpp
@@ -0,0 +1,1029 @@
+#include <uibase/ifiletree.h>
+
+#include <algorithm>
+#include <ranges>
+#include <span>
+#include <stack>
+
+#include <QRegularExpression>
+
+// FileTreeEntry:
+namespace MOBase
+{
+FileTreeEntry::FileTreeEntry(std::shared_ptr<const IFileTree> parent, QString name)
+ : m_Parent(parent), m_Name(name)
+{}
+
+QString FileTreeEntry::suffix() const
+{
+ const qsizetype idx = m_Name.lastIndexOf(".");
+ return (isDir() || idx == -1) ? "" : m_Name.mid(idx + 1);
+}
+
+bool FileTreeEntry::hasSuffix(QString suffix) const
+{
+ return this->suffix().compare(suffix, FileNameComparator::CaseSensitivity) == 0;
+}
+
+bool FileTreeEntry::hasSuffix(QStringList suffixes) const
+{
+ return suffixes.contains(suffix(), FileNameComparator::CaseSensitivity);
+}
+
+QString FileTreeEntry::pathFrom(std::shared_ptr<const IFileTree> tree,
+ QString sep) const
+{
+
+ // We will construct the path from right to left:
+ QString path = name();
+
+ auto p = parent();
+ while (p != nullptr && p != tree) {
+ // We need to check the parent, otherwize we are going to prepend the name
+ // and a / for the base, which we do not want.
+ if (p->parent() != nullptr) {
+ path = p->name() + sep + path;
+ }
+ p = p->parent();
+ }
+
+ return p == tree ? path : QString();
+}
+
+bool FileTreeEntry::detach()
+{
+ auto p = parent();
+ if (p == nullptr) {
+ return false;
+ }
+ return p->erase(shared_from_this()) != p->end();
+}
+
+bool FileTreeEntry::moveTo(std::shared_ptr<IFileTree> tree)
+{
+ return tree->insert(shared_from_this()) != tree->end();
+}
+
+std::shared_ptr<FileTreeEntry> FileTreeEntry::clone() const
+{
+ return createFileEntry(nullptr, name());
+}
+
+std::shared_ptr<FileTreeEntry>
+FileTreeEntry::createFileEntry(std::shared_ptr<const IFileTree> parent, QString name)
+{
+ return std::shared_ptr<FileTreeEntry>(new FileTreeEntry(parent, name));
+}
+} // namespace MOBase
+
+// IFileTree:
+namespace MOBase
+{
+
+/**
+ * Comparator for file entries.
+ */
+struct FileEntryComparator
+{
+ bool operator()(std::shared_ptr<FileTreeEntry> const& a,
+ std::shared_ptr<FileTreeEntry> const& b) const
+ {
+ if (a->isDir() && !b->isDir()) {
+ return true;
+ } else if (!a->isDir() && b->isDir()) {
+ return false;
+ } else {
+ return FileNameComparator::compare(a->name(), b->name()) < 0;
+ }
+ }
+};
+
+/**
+ * @brief Comparator that can be used to find entry matching the given name and
+ * file type.
+ */
+struct MatchEntryComparator
+{
+
+ MatchEntryComparator(QString const& name, FileTreeEntry::FileTypes matchTypes)
+ : m_Name(name), m_MatchTypes(matchTypes)
+ {}
+
+ bool operator()(const std::shared_ptr<const FileTreeEntry>& fileEntry) const
+ {
+ return m_MatchTypes.testFlag(fileEntry->fileType()) &&
+ fileEntry->compare(m_Name) == 0;
+ }
+
+ bool operator()(const std::shared_ptr<FileTreeEntry>& fileEntry) const
+ {
+ return m_MatchTypes.testFlag(fileEntry->fileType()) &&
+ fileEntry->compare(m_Name) == 0;
+ }
+
+private:
+ QString const& m_Name;
+ FileTreeEntry::FileTypes m_MatchTypes;
+};
+
+/**
+ *
+ */
+bool IFileTree::exists(QString path, FileTypes type) const
+{
+ return find(path, type) != nullptr;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::find(QString path, FileTypes type)
+{
+ return fetchEntry(splitPath(path), type);
+}
+std::shared_ptr<const FileTreeEntry> IFileTree::find(QString path, FileTypes type) const
+{
+ return fetchEntry(splitPath(path), type);
+}
+
+/**
+ *
+ */
+void IFileTree::walk(
+ std::function<WalkReturn(QString const&, std::shared_ptr<const FileTreeEntry>)>
+ callback,
+ QString sep) const
+{
+
+ std::stack<std::pair<QString, std::shared_ptr<const FileTreeEntry>>> stack;
+
+ // We start by pushing all the entries in this tree, this avoid having to do extra
+ // check later for avoid leading separator:
+ for (auto rit = rbegin(); rit != rend(); ++rit) {
+ stack.push({"", *rit});
+ }
+
+ while (!stack.empty()) {
+ auto [path, entry] = stack.top();
+ stack.pop();
+
+ auto res = callback(path, entry);
+ if (res == WalkReturn::STOP) {
+ break;
+ }
+ if (entry->isDir() && res != WalkReturn::SKIP) {
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ stack.push({path + tree->name() + sep, *rit});
+ }
+ }
+ }
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::addFile(QString path, bool replaceIfExists)
+{
+ QStringList parts = splitPath(path);
+
+ // Check if the file already exists:
+ auto existingEntry = fetchEntry(parts, IFileTree::FILE_OR_DIRECTORY);
+ if (!replaceIfExists && existingEntry != nullptr) {
+ return nullptr;
+ }
+
+ // Find or create the tree:
+ IFileTree* tree;
+
+ if (parts.size() > 1) {
+
+ // Create the tree:
+ std::shared_ptr<FileTreeEntry> treeEntry =
+ createTree(parts.begin(), parts.end() - 1);
+
+ // Early fail if the tree was not created:
+ if (treeEntry == nullptr) {
+ return nullptr;
+ }
+
+ tree = treeEntry->astree().get();
+ } else {
+ tree = this;
+ }
+
+ std::shared_ptr<FileTreeEntry> entry =
+ tree->makeFile(tree->astree(), parts[parts.size() - 1]);
+
+ // If makeFile returns a null pointer, it means we cannot create file:
+ if (entry == nullptr) {
+ return nullptr;
+ }
+
+ // Remove the existing files if there was one:
+ if (existingEntry) {
+ existingEntry->detach();
+ }
+
+ // Insert in the tree:
+ tree->entries().insert(
+ std::upper_bound(tree->begin(), tree->end(), entry, FileEntryComparator{}),
+ entry);
+
+ return entry;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::addDirectory(QString path)
+{
+ QStringList parts = splitPath(path);
+ return createTree(parts.begin(), parts.end());
+}
+
+/**
+ *
+ */
+IFileTree::iterator IFileTree::insert(std::shared_ptr<FileTreeEntry> entry,
+ InsertPolicy insertPolicy)
+{
+
+ // Check that this is not the current tree or a parent tree:
+ if (entry->isDir()) {
+ std::shared_ptr<IFileTree> tmp = astree();
+ while (tmp != nullptr) {
+ if (tmp == entry->astree()) {
+ return end();
+ }
+ tmp = tmp->parent();
+ }
+ }
+
+ // Check if there exists an entry with the same name:
+ auto existingIt = std::find_if(
+ begin(), end(), MatchEntryComparator{entry->name(), FILE_OR_DIRECTORY});
+
+ // Already in the tree?
+ if (existingIt != end() && *existingIt == entry) {
+ return existingIt;
+ }
+
+ auto insertionIt = end();
+
+ if (existingIt != end()) {
+ if (insertPolicy == InsertPolicy::FAIL_IF_EXISTS) {
+ return end();
+ }
+
+ // We replace if the policy is REPLACE or if the new and old entry are
+ // both files:
+ if (insertPolicy == InsertPolicy::REPLACE ||
+ ((*existingIt)->isFile() && entry->isFile())) {
+ if (beforeReplace(this, existingIt->get(), entry.get())) {
+ // Detach the old entry from its parent (not using .detach()
+ // to remove the entry since we are replacing it):
+ (*existingIt)->m_Parent.reset();
+ entries().erase(existingIt);
+
+ // insert at the right place
+ insertionIt = entries().insert(
+ std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry);
+ } else {
+ return end();
+ }
+ } else if ((*existingIt)->isFile() || entry->isFile()) {
+ // If we arrive here and one of the entry is a file, we fail:
+ return end();
+ } else {
+ // If we end up here, we know that the policy is MERGE and that both
+ // are directory that can be merged:
+ mergeTree((*existingIt)->astree(), entry->astree(), nullptr);
+ insertionIt = existingIt;
+ }
+ } else if (beforeInsert(this, entry.get())) {
+ insertionIt = entries().insert(
+ std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry);
+ }
+
+ // Remove the tree from its parent (parent() can be null if we are inserting
+ // a new tree):
+ if (entry->parent() != nullptr) {
+ entry->parent()->erase(entry);
+ }
+
+ // If the entry was actually inserted, we update its parent:
+ if (*insertionIt == entry) {
+ entry->m_Parent = astree();
+ }
+ // Otherwize, we reset it (if this was a merge operation):
+ else {
+ entry->m_Parent.reset();
+ }
+
+ return insertionIt;
+}
+
+/**
+ *
+ */
+bool IFileTree::move(std::shared_ptr<FileTreeEntry> entry, QString path,
+ InsertPolicy insertPolicy)
+{
+
+ // Check that this is not a parent tree:
+ if (entry->isDir()) {
+ std::shared_ptr<IFileTree> tmp = parent();
+ while (tmp != nullptr) {
+ if (tmp == entry->astree()) {
+ return false;
+ }
+ tmp = tmp->parent();
+ }
+ }
+
+ // Insert in folder or replace:
+ const bool insertFolder = path.isEmpty() || path.endsWith("/") || path.endsWith("\\");
+
+ // Retrieve the path:
+ QStringList parts = splitPath(path);
+
+ // Backup the entry name (in case the insertion fails), and update the
+ // name:
+ QString entryName = entry->m_Name;
+ if (!insertFolder) {
+ entry->m_Name = parts.takeLast();
+ }
+
+ // Find or create the tree:
+ IFileTree* tree;
+
+ if (!parts.isEmpty()) {
+
+ // Create the tree:
+ std::shared_ptr<FileTreeEntry> treeEntry = createTree(parts.begin(), parts.end());
+
+ // Early fail if the tree was not created:
+ if (treeEntry == nullptr) {
+ return false;
+ }
+
+ tree = treeEntry->astree().get();
+ } else {
+ tree = this;
+ }
+
+ // We try to insert, and if it fails we need to reset the name:
+ auto it = tree->insert(entry, insertPolicy);
+ if (it == tree->end()) {
+ entry->m_Name = entryName;
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry>
+IFileTree::copy(std::shared_ptr<const FileTreeEntry> entry, QString path,
+ InsertPolicy insertPolicy)
+{
+ // Note: If a conflict exists, the tree is cloned before checking the conflict, so
+ // this is not the most efficient way but copying tree should be pretty rare (and
+ // probably avoided anyway), and this allow us to use `move()` to do all the complex
+ // operations.
+ auto clone = entry->clone();
+ if (move(clone, path, insertPolicy)) {
+ return clone;
+ }
+ return nullptr;
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::merge(std::shared_ptr<IFileTree> source,
+ OverwritesType* overwrites)
+{
+
+ // Check that this is not a parent tree:
+ std::shared_ptr<IFileTree> tmp = astree();
+ while (tmp != nullptr) {
+ if (tmp == source) {
+ return MERGE_FAILED;
+ }
+ tmp = tmp->parent();
+ }
+
+ return mergeTree(astree(), source, overwrites);
+}
+
+/**
+ *
+ */
+IFileTree::iterator IFileTree::erase(std::shared_ptr<FileTreeEntry> entry)
+{
+
+ if (!beforeRemove(this, entry.get())) {
+ return end();
+ }
+
+ auto it = std::find(begin(), end(), entry);
+ if (it == end()) {
+ return it;
+ }
+ entry->m_Parent.reset();
+ return entries().erase(it);
+ ;
+}
+
+/**
+ *
+ */
+std::pair<IFileTree::iterator, std::shared_ptr<FileTreeEntry>>
+IFileTree::erase(QString name)
+{
+ auto it = std::find_if(begin(), end(), [&name](const auto& entry) {
+ return entry->compare(name) == 0;
+ });
+
+ if (it == end()) {
+ return {it, nullptr};
+ }
+
+ if (!beforeRemove(this, it->get())) {
+ return {end(), nullptr};
+ }
+
+ // Save the entry to return it:
+ auto entry = *it;
+ entry->m_Parent.reset();
+
+ return {entries().erase(it), entry};
+}
+
+/**
+ *
+ */
+bool IFileTree::clear()
+{
+ // Need to find the iterator up to which we should erase:
+ auto& entries_ = entries();
+ auto it = entries_.begin();
+ for (; it != entries_.end() && beforeRemove(this, it->get()); ++it) {
+ // Detach (but not remove from the vector):
+ (*it)->m_Parent.reset();
+ }
+ entries_.erase(entries_.begin(), it);
+ return empty();
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::removeAll(QStringList names)
+{
+ return removeIf([&names](auto& entry) {
+ return names.contains(entry->name(), Qt::CaseInsensitive);
+ });
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::removeIf(
+ std::function<bool(std::shared_ptr<FileTreeEntry> const&)> predicate)
+{
+ std::size_t osize = size();
+ auto& en = entries();
+ // Cannot use begin() and end() directly because those are immutable iterators:
+ en.erase(std::remove_if(en.begin(), en.end(),
+ [this, &predicate](auto& entry) {
+ return beforeRemove(this, entry.get()) && predicate(entry);
+ }),
+ en.end());
+ return osize - size();
+}
+
+/**
+ *
+ */
+QStringList IFileTree::splitPath(QString path)
+{
+ // Using raw \\ instead of QDir::separator() since we are replacing by /
+ // anyway, and this avoid pulling an extra header (like QDir) only
+ // for the separator.
+ return path.replace("\\", "/").split("/", Qt::SkipEmptyParts);
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeReplace(IFileTree const*, FileTreeEntry const*,
+ FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeInsert(IFileTree const*, FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeRemove(IFileTree const*, FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::mergeTree(std::shared_ptr<IFileTree> destination,
+ std::shared_ptr<IFileTree> source,
+ OverwritesType* overwrites)
+{
+
+ const auto comp = FileEntryComparator{};
+
+ // Number of overwritten entries:
+ std::size_t noverwrites = 0;
+
+ // Note: Using the iterators from the vector directly since the other ones
+ // cannot be assigned to.
+ auto &dstEntries = destination->entries(), &srcEntries = source->entries();
+
+ // Note: Since entries are not sorted by name but by type (file/directory)
+ // then name, there is no fast way to check for conflict.
+ for (auto& srcEntry : srcEntries) {
+
+ // Try to find an exact match (name and type) - This iterator also
+ // serve to know where the entry should be inserted:
+ auto dstIt = std::lower_bound(dstEntries.begin(), dstEntries.end(), srcEntry, comp);
+
+ // Exact match found:
+ if (dstIt != dstEntries.end() && (*dstIt)->compare(srcEntry->name()) == 0 &&
+ (*dstIt)->isFile() == srcEntry->isFile()) {
+
+ // Both directory, we merge:
+ if ((*dstIt)->isDir() && srcEntry->isDir()) {
+ noverwrites += mergeTree((*dstIt)->astree(), srcEntry->astree(), overwrites);
+
+ // Detach the entry:
+ srcEntry->m_Parent.reset();
+ }
+ // Otherwize, check if the source can replace the destination:
+ else if (beforeReplace(destination.get(), dstIt->get(), srcEntry.get())) {
+ // Remove the parent:
+ auto dstEntry = *dstIt;
+ dstEntry->m_Parent.reset();
+
+ // Update overwrites information:
+ noverwrites++;
+ if (overwrites != nullptr) {
+ overwrites->insert({dstEntry, srcEntry});
+ }
+
+ // Replace the destination:
+ *dstIt = srcEntry;
+ srcEntry->m_Parent = destination;
+ }
+ // If not, fails:
+ else {
+ return MERGE_FAILED;
+ }
+ } else {
+ // If we did not find a match, the only way to check is to look
+ // through the vector:
+ auto conflictIt = std::find_if(dstEntries.begin(), dstEntries.end(),
+ [name = srcEntry->name()](auto const& dstEntry) {
+ return dstEntry->compare(name) == 0;
+ });
+
+ // Conflict (note that here both entries are of different types, so no need to
+ // check if we replace or merge):
+ int deleteIndex = -1;
+ if (conflictIt != dstEntries.end()) {
+
+ // We check if we can replace the entry:
+ if (!beforeReplace(destination.get(), conflictIt->get(), srcEntry.get())) {
+ return MERGE_FAILED;
+ }
+
+ // We need to store the index because the insert() will mess up the
+ // iterators:
+ deleteIndex = static_cast<int>(conflictIt - std::begin(dstEntries));
+ if (dstIt < conflictIt) {
+ deleteIndex += 1;
+ }
+
+ // Detach the conflicting entry (we erase it later, after the insertion):
+ (*conflictIt)->m_Parent.reset();
+
+ // Update overwrites information:
+ noverwrites++;
+ if (overwrites != nullptr) {
+ overwrites->insert({*conflictIt, srcEntry});
+ }
+ }
+ // No conflict, we still have to check if we can insert:
+ else if (!beforeInsert(destination.get(), srcEntry.get())) {
+ return MERGE_FAILED;
+ }
+
+ // Insert the entry using the previous iterator:
+ dstEntries.insert(dstIt, srcEntry);
+
+ // We delete here:
+ if (deleteIndex != -1) {
+ dstEntries.erase(dstEntries.begin() + deleteIndex);
+ }
+
+ // Update the parent:
+ srcEntry->m_Parent = destination;
+ }
+ }
+
+ // Clear the sources:
+ srcEntries.clear();
+
+ return noverwrites;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::createOrphanTree(QString name) const
+{
+ auto directory = makeDirectory(nullptr, name);
+ if (directory != nullptr) {
+ directory->m_Populated = true;
+ }
+ return directory;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::fetchEntry(QStringList path,
+ FileTypes matchTypes)
+{
+ return std::const_pointer_cast<FileTreeEntry>(
+ const_cast<const IFileTree*>(this)->fetchEntry(path, matchTypes));
+}
+std::shared_ptr<const FileTreeEntry> IFileTree::fetchEntry(QStringList const& path,
+ FileTypes matchTypes) const
+{
+ // Check to ensure that the path contains at least one element:
+ if (path.isEmpty()) {
+ return nullptr;
+ }
+
+ // Early check:
+ if (path[path.size() - 1].startsWith("*")) {
+ return nullptr;
+ }
+
+ const IFileTree* tree = this;
+ auto it = std::begin(path);
+ for (; tree != nullptr && it != std::end(path) - 1; ++it) {
+ // Special cases:
+ if (*it == ".") {
+ continue;
+ } else if (*it == "..") {
+ tree = tree->parent().get();
+ } else {
+ // Find the entry at the current level:
+ auto entryIt = std::find_if(tree->begin(), tree->end(),
+ MatchEntryComparator{*it, IFileTree::DIRECTORY});
+
+ // Early exists if the entry does not exist or is not a directory:
+ if (entryIt == tree->end()) {
+ tree = nullptr;
+ } else {
+ tree = (*entryIt)->astree().get();
+ }
+ }
+ }
+
+ if (tree == nullptr) {
+ return nullptr;
+ }
+
+ // We have the final tree:
+ auto entryIt =
+ std::find_if(tree->begin(), tree->end(), MatchEntryComparator{*it, matchTypes});
+ auto bIt = tree->end();
+ return entryIt == bIt ? nullptr : *entryIt;
+}
+
+/**
+ * @brief Create a new file under this tree.
+ *
+ * @param parent The current tree, without const-qualification.
+ * @param name Name of the file.
+ * @param time Modification time of the file.
+ *
+ * @return the created file.
+ */
+std::shared_ptr<FileTreeEntry>
+IFileTree::makeFile(std::shared_ptr<const IFileTree> parent, QString name) const
+{
+ return createFileEntry(parent, name);
+}
+
+/**
+ *
+ */
+IFileTree::IFileTree() {}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::clone() const
+{
+ std::shared_ptr<IFileTree> tree = doClone();
+
+ // Don't copy not populated tree, it is not useful:
+ if (m_Populated) {
+ tree->m_Populated = true;
+ auto& tentries = tree->m_Entries;
+ for (auto e : entries()) {
+ auto ce = e->clone();
+ ce->m_Parent = tree;
+ tentries.push_back(ce);
+ }
+ }
+
+ return tree;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::createTree(QStringList::const_iterator begin,
+ QStringList::const_iterator end)
+{
+ // The current tree and entry:
+ std::shared_ptr<IFileTree> tree = astree();
+ for (auto it = begin; tree != nullptr && it != end; ++it) {
+ // Special cases:
+ if (*it == ".") {
+ continue;
+ } else if (*it == "..") {
+ // parent() returns nullptr if it does not exist, so no
+ // check required:
+ tree = parent();
+ } else {
+
+ // Check if the entry exists (looking for both files and directories
+ // because we don't want to override a file):
+ auto entryIt =
+ std::find_if(tree->begin(), tree->end(),
+ MatchEntryComparator{*it, IFileTree::FILE_OR_DIRECTORY});
+
+ // Create if it does not:
+ if (entryIt == tree->end()) {
+ auto newTree = tree->makeDirectory(tree, *it);
+
+ // If makeDirectory returns a null pointer, it means we cannot create tree.
+ if (newTree == nullptr) {
+ tree = nullptr;
+ break;
+ }
+
+ // The tree is empty so already populated:
+ newTree->m_Populated = true;
+
+ tree->entries().insert(std::upper_bound(tree->begin(), tree->end(), newTree,
+ FileEntryComparator{}),
+ newTree);
+ tree = newTree;
+ } else if ((*entryIt)->isDir()) {
+ tree = (*entryIt)->astree();
+ } else { // Cannot go further:
+ tree = nullptr;
+ }
+ }
+ }
+
+ return tree;
+}
+
+/**
+ * @brief Retrieve the vector of entries after populating it if required.
+ *
+ * @return the vector of entries.
+ */
+std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries()
+{
+ std::call_once(m_OnceFlag, [this]() {
+ populate();
+ });
+ return m_Entries;
+}
+const std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries() const
+{
+ std::call_once(m_OnceFlag, [this]() {
+ populate();
+ });
+ return m_Entries;
+}
+
+/**
+ * @brief Populate the internal vectors and update the flag.
+ */
+void IFileTree::populate() const
+{
+ // Need to check m_Populated again here since the tree can be populated without
+ // a call to entries() (e.g., on copy/orphanTree):
+ if (!m_Populated) {
+ if (!doPopulate(astree(), m_Entries)) {
+ std::sort(std::begin(m_Entries), std::end(m_Entries), FileEntryComparator{});
+ }
+ m_Populated = true;
+ }
+}
+
+// walk and glob with generator
+
+std::generator<std::shared_ptr<const FileTreeEntry>>
+walk(std::shared_ptr<const IFileTree> fileTree)
+{
+ std::stack<std::shared_ptr<const FileTreeEntry>> stack;
+
+ // we start by pushing all the entries in this tree, this avoid having to do extra
+ // check later to avoid leading separator
+ for (auto rit = fileTree->rbegin(); rit != fileTree->rend(); ++rit) {
+ stack.push(*rit);
+ }
+
+ while (!stack.empty()) {
+ auto entry = stack.top();
+ stack.pop();
+
+ co_yield entry;
+
+ if (entry->isDir()) {
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ stack.push(*rit);
+ }
+ }
+ }
+}
+
+namespace
+{
+ using glob_stack_t = std::stack<
+ std::pair<std::shared_ptr<const FileTreeEntry>, std::span<QRegularExpression>>>;
+
+ // check the given entry against the list of patterns and add new (entry, patterns) to
+ // the given stack
+ //
+ std::generator<std::shared_ptr<const FileTreeEntry>>
+ ifiletree_glob_impl_step(glob_stack_t& glob_stack,
+ std::shared_ptr<const FileTreeEntry> entry,
+ std::span<QRegularExpression> const& patterns)
+ {
+ // no more patterns, nothing to do
+ if (patterns.size() == 0) {
+ co_return;
+ }
+
+ // special handling if the pattern is empty (correspond to a '**' glob)
+ if (patterns[0].pattern() == "") {
+
+ // if there are more patterns after '**', we need to check the entry again, e.g.,
+ // if the entry name is 'x' and the pattern is '**/x' to match it
+ if (patterns.size() != 1) {
+ glob_stack.emplace(entry, patterns.subspan(1));
+ }
+
+ // if the entry is a file, there is nothing to do with '**'
+ if (entry->isFile()) {
+ co_return;
+ }
+
+ // if this is the end of the patterns list, we need to yield the current entry
+ // since it is a directory
+ if (patterns.size() == 1) {
+ co_yield entry;
+ }
+
+ // recurse over childs, but for directories, we need to keep the leading '**' in
+ // the list of patterns since '**' can match multiple level of directories
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ glob_stack.emplace(*rit, (*rit)->isDir() ? patterns : patterns.subspan(1));
+ }
+ }
+ // otherwise (if the first patterns is not '**'), we simply check if we have a match
+ else if (patterns[0].match(entry->name()).hasMatch()) {
+ // this was the last pattern and we have a match, so we yield the current entry,
+ // not that this will yield intermediate matching directory, but this is expected
+ if (patterns.size() == 1) {
+ co_yield entry;
+ }
+
+ // if the entry is not a directory, we have nothing more to do
+ if (entry->isFile()) {
+ co_return;
+ }
+
+ // if all that remain after this pattern is a '**', we need to yield the current
+ // entry since '**' can also match an empty succession of directories, e.g. 'a/b'
+ // is matched by 'a/b/**'
+ if (patterns.size() == 2 && patterns[1].pattern() == "") {
+ co_yield entry;
+ }
+
+ // we then need to recurse over
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ glob_stack.emplace(*rit, patterns.subspan(1));
+ }
+ }
+ }
+
+ // main function for IFileTree::glob - this function simply manages the stack of of
+ // entry / patterns to handle, and then falls back to ifiletree_glob_impl_step
+ //
+ std::generator<std::shared_ptr<const FileTreeEntry>>
+ ifiletree_glob_impl(std::shared_ptr<const FileTreeEntry> entry,
+ std::span<QRegularExpression> const& patterns)
+ {
+ glob_stack_t stack;
+ stack.emplace(entry, patterns);
+
+ while (!stack.empty()) {
+ const auto [childEntry, childPatterns] = stack.top();
+ stack.pop();
+ co_yield std::ranges::elements_of(
+ ifiletree_glob_impl_step(stack, childEntry, childPatterns));
+ }
+ }
+
+} // namespace
+
+/**
+ *
+ */
+std::generator<std::shared_ptr<const FileTreeEntry>>
+glob(std::shared_ptr<const IFileTree> fileTree, QString pattern,
+ GlobPatternType patternType)
+{
+ // replace \\ by / to simply handling
+ pattern = pattern.trimmed().replace("\\", "/");
+
+ // reduce successions of **/** to **, this makes it easier to handle it in the
+ // actual implementation
+ pattern = pattern.replace(QRegularExpression("(\\*\\*/)*\\*\\*"), "**");
+
+ // we are going to match directly starting from the child of the tree, so we need
+ // to handle the root here
+ //
+ // note: this is the only pattern that can match the tree itself
+ if (pattern == "**") {
+ co_yield fileTree;
+ }
+
+ // split pattern into blocks, we keep
+ const auto regexOptions = FileNameComparator::CaseSensitivity == Qt::CaseInsensitive
+ ? QRegularExpression::CaseInsensitiveOption
+ : QRegularExpression::NoPatternOption;
+ std::vector<QRegularExpression> patterns;
+ for (const auto& part : pattern.split("/")) {
+ if (part == "**") {
+ // for '**' pattern, push an empty regex
+ patterns.emplace_back();
+ } else {
+ const auto regexpPattern =
+ patternType == GlobPatternType::GLOB
+ ? QRegularExpression::wildcardToRegularExpression(
+ part, QRegularExpression::NonPathWildcardConversion)
+ : part;
+ const QRegularExpression regex(regexpPattern, regexOptions);
+
+ if (!regex.isValid()) {
+ throw InvalidGlobPatternException(regex.errorString());
+ }
+
+ patterns.push_back(regex);
+ }
+ }
+
+ // fall back to the main glob function for the child of this tree
+ for (const auto& child : *fileTree) {
+ co_yield std::ranges::elements_of(ifiletree_glob_impl(child, patterns));
+ }
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/imodrepositorybridge.cpp b/libs/uibase/src/imodrepositorybridge.cpp
new file mode 100644
index 0000000..9b610b0
--- /dev/null
+++ b/libs/uibase/src/imodrepositorybridge.cpp
@@ -0,0 +1 @@
+#include <uibase/imodrepositorybridge.h>
diff --git a/libs/uibase/src/imoinfo.cpp b/libs/uibase/src/imoinfo.cpp
new file mode 100644
index 0000000..faa2db9
--- /dev/null
+++ b/libs/uibase/src/imoinfo.cpp
@@ -0,0 +1,23 @@
+#include <uibase/imoinfo.h>
+
+namespace MOBase
+{
+
+static QString g_pluginDataPath;
+
+QString IOrganizer::getPluginDataPath()
+{
+ return g_pluginDataPath;
+}
+
+} // namespace MOBase
+
+namespace MOBase::details
+{
+
+void setPluginDataPath(const QString& s)
+{
+ g_pluginDataPath = s;
+}
+
+} // namespace MOBase::details
diff --git a/libs/uibase/src/json.cpp b/libs/uibase/src/json.cpp
new file mode 100644
index 0000000..49d5fb3
--- /dev/null
+++ b/libs/uibase/src/json.cpp
@@ -0,0 +1,557 @@
+/**
+ * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and
+ * vice-versa. Copyright (C) 2011 Eeli Reilin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * \file json.cpp
+ */
+
+#include <uibase/json.h>
+
+namespace QtJson
+{
+static QString sanitizeString(QString str);
+static QByteArray join(const QList<QByteArray>& list, const QByteArray& sep);
+static QVariant parseValue(const QString& json, int& index, bool& success);
+static QVariant parseObject(const QString& json, int& index, bool& success);
+static QVariant parseArray(const QString& json, int& index, bool& success);
+static QVariant parseString(const QString& json, int& index, bool& success);
+static QVariant parseNumber(const QString& json, int& index);
+static int lastIndexOfNumber(const QString& json, int index);
+static void eatWhitespace(const QString& json, int& index);
+static int lookAhead(const QString& json, int index);
+static int nextToken(const QString& json, int& index);
+
+template <typename T>
+QByteArray serializeMap(const T& map, bool& success)
+{
+ QByteArray str = "{ ";
+ QList<QByteArray> pairs;
+ for (typename T::const_iterator it = map.begin(), itend = map.end(); it != itend;
+ ++it) {
+ QByteArray serializedValue = serialize(it.value());
+ if (serializedValue.isNull()) {
+ success = false;
+ break;
+ }
+ pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue;
+ }
+
+ str += join(pairs, ", ");
+ str += " }";
+ return str;
+}
+
+/**
+ * parse
+ */
+QVariant parse(const QString& json)
+{
+ bool success = true;
+ return parse(json, success);
+}
+
+/**
+ * parse
+ */
+QVariant parse(const QString& json, bool& success)
+{
+ success = true;
+
+ // Return an empty QVariant if the JSON data is either null or empty
+ if (!json.isNull() || !json.isEmpty()) {
+ QString data = json;
+ // We'll start from index 0
+ int index = 0;
+
+ // Parse the first value
+ QVariant value = parseValue(data, index, success);
+
+ // Return the parsed value
+ return value;
+ } else {
+ // Return the empty QVariant
+ return QVariant();
+ }
+}
+
+QByteArray serialize(const QVariant& data)
+{
+ bool success = true;
+ return serialize(data, success);
+}
+
+QByteArray serialize(const QVariant& data, bool& success)
+{
+ QByteArray str;
+ success = true;
+
+ if (!data.isValid()) { // invalid or null?
+ str = "null";
+ } else if ((data.typeId() == QMetaType::Type::QVariantList) ||
+ (data.typeId() == QMetaType::Type::QStringList)) { // variant is a list?
+ QList<QByteArray> values;
+ const QVariantList list = data.toList();
+ Q_FOREACH (const QVariant& v, list) {
+ QByteArray serializedValue = serialize(v);
+ if (serializedValue.isNull()) {
+ success = false;
+ break;
+ }
+ values << serializedValue;
+ }
+
+ str = "[ " + join(values, ", ") + " ]";
+ } else if (data.typeId() == QMetaType::Type::QVariantHash) { // variant is a hash?
+ str = serializeMap<>(data.toHash(), success);
+ } else if (data.typeId() == QMetaType::Type::QVariantMap) { // variant is a map?
+ str = serializeMap<>(data.toMap(), success);
+ } else if ((data.typeId() == QMetaType::Type::QString) ||
+ (data.typeId() ==
+ QMetaType::Type::QByteArray)) { // a string or a byte array?
+ str = sanitizeString(data.toString()).toUtf8();
+ } else if (data.typeId() == QMetaType::Type::Double) { // double?
+ double value = data.toDouble();
+ if ((value - value) == 0.0) {
+ str = QByteArray::number(value, 'g');
+ if (!str.contains(".") && !str.contains("e")) {
+ str += ".0";
+ }
+ } else {
+ success = false;
+ }
+ } else if (data.typeId() == QMetaType::Type::Bool) { // boolean value?
+ str = data.toBool() ? "true" : "false";
+ } else if (data.typeId() == QMetaType::Type::ULongLong) { // large unsigned number?
+ str = QByteArray::number(data.value<qulonglong>());
+ } else if (data.canConvert<qlonglong>()) { // any signed number?
+ str = QByteArray::number(data.value<qlonglong>());
+ } else if (data.canConvert<long>()) { // TODO: this code is never executed
+ str = QString::number(data.value<long>()).toUtf8();
+ } else if (data.canConvert<QString>()) { // can value be converted to string?
+ // this will catch QDate, QDateTime, QUrl, ...
+ str = sanitizeString(data.toString()).toUtf8();
+ } else {
+ success = false;
+ }
+
+ if (success) {
+ return str;
+ } else {
+ return QByteArray();
+ }
+}
+
+QString serializeStr(const QVariant& data)
+{
+ return QString::fromUtf8(serialize(data));
+}
+
+QString serializeStr(const QVariant& data, bool& success)
+{
+ return QString::fromUtf8(serialize(data, success));
+}
+
+/**
+ * \enum JsonToken
+ */
+enum JsonToken
+{
+ JsonTokenNone = 0,
+ JsonTokenCurlyOpen = 1,
+ JsonTokenCurlyClose = 2,
+ JsonTokenSquaredOpen = 3,
+ JsonTokenSquaredClose = 4,
+ JsonTokenColon = 5,
+ JsonTokenComma = 6,
+ JsonTokenString = 7,
+ JsonTokenNumber = 8,
+ JsonTokenTrue = 9,
+ JsonTokenFalse = 10,
+ JsonTokenNull = 11
+};
+
+static QString sanitizeString(QString str)
+{
+ str.replace(QLatin1String("\\"), QLatin1String("\\\\"));
+ str.replace(QLatin1String("\""), QLatin1String("\\\""));
+ str.replace(QLatin1String("\b"), QLatin1String("\\b"));
+ str.replace(QLatin1String("\f"), QLatin1String("\\f"));
+ str.replace(QLatin1String("\n"), QLatin1String("\\n"));
+ str.replace(QLatin1String("\r"), QLatin1String("\\r"));
+ str.replace(QLatin1String("\t"), QLatin1String("\\t"));
+ return QString(QLatin1String("\"%1\"")).arg(str);
+}
+
+static QByteArray join(const QList<QByteArray>& list, const QByteArray& sep)
+{
+ QByteArray res;
+ Q_FOREACH (const QByteArray& i, list) {
+ if (!res.isEmpty()) {
+ res += sep;
+ }
+ res += i;
+ }
+ return res;
+}
+
+/**
+ * parseValue
+ */
+static QVariant parseValue(const QString& json, int& index, bool& success)
+{
+ // Determine what kind of data we should parse by
+ // checking out the upcoming token
+ switch (lookAhead(json, index)) {
+ case JsonTokenString:
+ return parseString(json, index, success);
+ case JsonTokenNumber:
+ return parseNumber(json, index);
+ case JsonTokenCurlyOpen:
+ return parseObject(json, index, success);
+ case JsonTokenSquaredOpen:
+ return parseArray(json, index, success);
+ case JsonTokenTrue:
+ nextToken(json, index);
+ return QVariant(true);
+ case JsonTokenFalse:
+ nextToken(json, index);
+ return QVariant(false);
+ case JsonTokenNull:
+ nextToken(json, index);
+ return QVariant();
+ case JsonTokenNone:
+ break;
+ }
+
+ // If there were no tokens, flag the failure and return an empty QVariant
+ success = false;
+ return QVariant();
+}
+
+/**
+ * parseObject
+ */
+static QVariant parseObject(const QString& json, int& index, bool& success)
+{
+ QVariantMap map;
+ int token;
+
+ // Get rid of the whitespace and increment index
+ nextToken(json, index);
+
+ // Loop through all of the key/value pairs of the object
+ bool done = false;
+ while (!done) {
+ // Get the upcoming token
+ token = lookAhead(json, index);
+
+ if (token == JsonTokenNone) {
+ success = false;
+ return QVariantMap();
+ } else if (token == JsonTokenComma) {
+ nextToken(json, index);
+ } else if (token == JsonTokenCurlyClose) {
+ nextToken(json, index);
+ return map;
+ } else {
+ // Parse the key/value pair's name
+ QString name = parseString(json, index, success).toString();
+
+ if (!success) {
+ return QVariantMap();
+ }
+
+ // Get the next token
+ token = nextToken(json, index);
+
+ // If the next token is not a colon, flag the failure
+ // return an empty QVariant
+ if (token != JsonTokenColon) {
+ success = false;
+ return QVariant(QVariantMap());
+ }
+
+ // Parse the key/value pair's value
+ QVariant value = parseValue(json, index, success);
+
+ if (!success) {
+ return QVariantMap();
+ }
+
+ // Assign the value to the key in the map
+ map[name] = value;
+ }
+ }
+
+ // Return the map successfully
+ return QVariant(map);
+}
+
+/**
+ * parseArray
+ */
+static QVariant parseArray(const QString& json, int& index, bool& success)
+{
+ QVariantList list;
+
+ nextToken(json, index);
+
+ bool done = false;
+ while (!done) {
+ int token = lookAhead(json, index);
+
+ if (token == JsonTokenNone) {
+ success = false;
+ return QVariantList();
+ } else if (token == JsonTokenComma) {
+ nextToken(json, index);
+ } else if (token == JsonTokenSquaredClose) {
+ nextToken(json, index);
+ break;
+ } else {
+ QVariant value = parseValue(json, index, success);
+ if (!success) {
+ return QVariantList();
+ }
+ list.push_back(value);
+ }
+ }
+
+ return QVariant(list);
+}
+
+/**
+ * parseString
+ */
+static QVariant parseString(const QString& json, int& index, bool& success)
+{
+ QString s;
+ QChar c;
+
+ eatWhitespace(json, index);
+
+ c = json[index++];
+
+ bool complete = false;
+ while (!complete) {
+ if (index == json.size()) {
+ break;
+ }
+
+ c = json[index++];
+
+ if (c == '\"') {
+ complete = true;
+ break;
+ } else if (c == '\\') {
+ if (index == json.size()) {
+ break;
+ }
+
+ c = json[index++];
+
+ if (c == '\"') {
+ s.append('\"');
+ } else if (c == '\\') {
+ s.append('\\');
+ } else if (c == '/') {
+ s.append('/');
+ } else if (c == 'b') {
+ s.append('\b');
+ } else if (c == 'f') {
+ s.append('\f');
+ } else if (c == 'n') {
+ s.append('\n');
+ } else if (c == 'r') {
+ s.append('\r');
+ } else if (c == 't') {
+ s.append('\t');
+ } else if (c == 'u') {
+ qsizetype remainingLength = json.size() - index;
+ if (remainingLength >= 4) {
+ QString unicodeStr = json.mid(index, 4);
+
+ int symbol = unicodeStr.toInt(0, 16);
+
+ s.append(QChar(symbol));
+
+ index += 4;
+ } else {
+ break;
+ }
+ }
+ } else {
+ s.append(c);
+ }
+ }
+
+ if (!complete) {
+ success = false;
+ return QVariant();
+ }
+
+ return QVariant(s);
+}
+
+/**
+ * parseNumber
+ */
+static QVariant parseNumber(const QString& json, int& index)
+{
+ eatWhitespace(json, index);
+
+ int lastIndex = lastIndexOfNumber(json, index);
+ int charLength = (lastIndex - index) + 1;
+ QString numberStr;
+
+ numberStr = json.mid(index, charLength);
+
+ index = lastIndex + 1;
+ bool ok;
+
+ if (numberStr.contains('.')) {
+ return QVariant(numberStr.toDouble(nullptr));
+ } else if (numberStr.startsWith('-')) {
+ int i = numberStr.toInt(&ok);
+ if (!ok) {
+ qlonglong ll = numberStr.toLongLong(&ok);
+ return ok ? ll : QVariant(numberStr);
+ }
+ return i;
+ } else {
+ uint u = numberStr.toUInt(&ok);
+ if (!ok) {
+ qulonglong ull = numberStr.toULongLong(&ok);
+ return ok ? ull : QVariant(numberStr);
+ }
+ return u;
+ }
+}
+
+/**
+ * lastIndexOfNumber
+ */
+static int lastIndexOfNumber(const QString& json, int index)
+{
+ int lastIndex;
+
+ for (lastIndex = index; lastIndex < json.size(); lastIndex++) {
+ if (QString("0123456789+-.eE").indexOf(json[lastIndex]) == -1) {
+ break;
+ }
+ }
+
+ return lastIndex - 1;
+}
+
+/**
+ * eatWhitespace
+ */
+static void eatWhitespace(const QString& json, int& index)
+{
+ for (; index < json.size(); index++) {
+ if (QString(" \t\n\r").indexOf(json[index]) == -1) {
+ break;
+ }
+ }
+}
+
+/**
+ * lookAhead
+ */
+static int lookAhead(const QString& json, int index)
+{
+ int saveIndex = index;
+ return nextToken(json, saveIndex);
+}
+
+/**
+ * nextToken
+ */
+static int nextToken(const QString& json, int& index)
+{
+ eatWhitespace(json, index);
+
+ if (index == json.size()) {
+ return JsonTokenNone;
+ }
+
+ QChar c = json[index];
+ index++;
+ switch (c.toLatin1()) {
+ case '{':
+ return JsonTokenCurlyOpen;
+ case '}':
+ return JsonTokenCurlyClose;
+ case '[':
+ return JsonTokenSquaredOpen;
+ case ']':
+ return JsonTokenSquaredClose;
+ case ',':
+ return JsonTokenComma;
+ case '"':
+ return JsonTokenString;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '-':
+ return JsonTokenNumber;
+ case ':':
+ return JsonTokenColon;
+ }
+ index--; // ^ WTF?
+
+ qsizetype remainingLength = json.size() - index;
+
+ // True
+ if (remainingLength >= 4) {
+ if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' &&
+ json[index + 3] == 'e') {
+ index += 4;
+ return JsonTokenTrue;
+ }
+ }
+
+ // False
+ if (remainingLength >= 5) {
+ if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' &&
+ json[index + 3] == 's' && json[index + 4] == 'e') {
+ index += 5;
+ return JsonTokenFalse;
+ }
+ }
+
+ // Null
+ if (remainingLength >= 4) {
+ if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' &&
+ json[index + 3] == 'l') {
+ index += 4;
+ return JsonTokenNull;
+ }
+ }
+
+ return JsonTokenNone;
+}
+} // namespace QtJson
diff --git a/libs/uibase/src/lineeditclear.cpp b/libs/uibase/src/lineeditclear.cpp
new file mode 100644
index 0000000..476c874
--- /dev/null
+++ b/libs/uibase/src/lineeditclear.cpp
@@ -0,0 +1,50 @@
+/****************************************************************************
+**
+** Copyright (c) 2007 Trolltech ASA <info@trolltech.com>
+**
+** Use, modification and distribution is allowed without limitation,
+** warranty, liability or support of any kind.
+**
+****************************************************************************/
+
+#include <uibase/lineeditclear.h>
+#include <QStyle>
+#include <QToolButton>
+
+namespace MOBase
+{
+
+LineEditClear::LineEditClear(QWidget* parent) : QLineEdit(parent)
+{
+ clearButton = new QToolButton(this);
+ QPixmap pixmap(":/MO/gui/edit_clear");
+ clearButton->setIcon(QIcon(pixmap));
+ clearButton->setIconSize(pixmap.size());
+ clearButton->setCursor(Qt::ArrowCursor);
+ clearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }");
+ clearButton->hide();
+ connect(clearButton, SIGNAL(clicked()), this, SLOT(clear()));
+ connect(this, SIGNAL(textChanged(const QString&)), this,
+ SLOT(updateCloseButton(const QString&)));
+ int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
+ setStyleSheet(QString("QLineEdit { padding-right: %1px; } ")
+ .arg(clearButton->sizeHint().width() + frameWidth + 1));
+ /* QSize msz = minimumSizeHint();
+ setMinimumSize(qMax(msz.width(), clearButton->sizeHint().height() + frameWidth * 2 +
+ 2), qMax(msz.height(), clearButton->sizeHint().height() + frameWidth * 2 + 2));*/
+}
+
+void LineEditClear::resizeEvent(QResizeEvent*)
+{
+ QSize sz = clearButton->sizeHint();
+ int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
+ clearButton->move(rect().right() - frameWidth - sz.width(),
+ (rect().bottom() + 1 - sz.height()) / 2);
+}
+
+void LineEditClear::updateCloseButton(const QString& text)
+{
+ clearButton->setVisible(!text.isEmpty());
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/linklabel.cpp b/libs/uibase/src/linklabel.cpp
new file mode 100644
index 0000000..c4d09f1
--- /dev/null
+++ b/libs/uibase/src/linklabel.cpp
@@ -0,0 +1,27 @@
+#include <uibase/linklabel.h>
+#include <QApplication>
+
+QColor LinkLabel::m_linkColor;
+
+LinkLabel::LinkLabel(QWidget* parent) : QLabel(parent) {}
+
+QColor LinkLabel::linkColor() const
+{
+ return m_linkColor;
+}
+
+void LinkLabel::setLinkColor(const QColor& c)
+{
+ if (m_linkColor != c) {
+ m_linkColor = c;
+
+ // setting link color on the global palette; qt doesn't seem to support
+ // per-widget colors for links
+ if (qApp) {
+ auto p = qApp->palette();
+ p.setColor(QPalette::Link, c);
+ p.setColor(QPalette::LinkVisited, c);
+ qApp->setPalette(p);
+ }
+ }
+}
diff --git a/libs/uibase/src/log.cpp b/libs/uibase/src/log.cpp
new file mode 100644
index 0000000..a44c9e3
--- /dev/null
+++ b/libs/uibase/src/log.cpp
@@ -0,0 +1,411 @@
+#include <uibase/log.h>
+#include "pch.h"
+#include <uibase/utility.h>
+#include <iostream>
+
+#include <algorithm>
+#include <locale>
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4365)
+#endif
+
+#ifdef _WIN32
+#define SPDLOG_WCHAR_FILENAMES 1
+#endif
+
+#include <spdlog/logger.h>
+#include <spdlog/sinks/base_sink.h>
+#include <spdlog/sinks/basic_file_sink.h>
+#include <spdlog/sinks/daily_file_sink.h>
+#include <spdlog/sinks/dist_sink.h>
+#include <spdlog/sinks/rotating_file_sink.h>
+#include <spdlog/sinks/stdout_color_sinks.h>
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+namespace MOBase::log
+{
+
+namespace fs = std::filesystem;
+static std::unique_ptr<Logger> g_default;
+
+spdlog::level::level_enum toSpdlog(Levels lv)
+{
+ switch (lv) {
+ case Debug:
+ return spdlog::level::debug;
+
+ case Warning:
+ return spdlog::level::warn;
+
+ case Error:
+ return spdlog::level::err;
+
+ case Info: // fall-through
+ default:
+ return spdlog::level::info;
+ }
+}
+
+Levels fromSpdlog(spdlog::level::level_enum lv)
+{
+ switch (lv) {
+ case spdlog::level::trace:
+ case spdlog::level::debug:
+ return Debug;
+
+ case spdlog::level::warn:
+ return Warning;
+
+ case spdlog::level::critical: // fall-through
+ case spdlog::level::err:
+ return Error;
+
+ case spdlog::level::info: // fall-through
+ case spdlog::level::off:
+ case spdlog::level::n_levels: // to please MSVC
+ default:
+ return Info;
+ }
+}
+
+class CallbackSink : public spdlog::sinks::base_sink<std::mutex>
+{
+public:
+ CallbackSink(Callback* f) : m_f(f) {}
+
+ void setCallback(Callback* f) { m_f = f; }
+
+protected:
+ void sink_it_(const spdlog::details::log_msg& m) override
+ {
+ thread_local bool active = false;
+
+ if (active) {
+ // trying to log from a log callback, ignoring
+ return;
+ }
+
+ if (!m_f) {
+ // disabled
+ return;
+ }
+
+ try {
+ auto g = Guard([&] {
+ active = false;
+ });
+ active = true;
+
+ Entry e;
+ e.time = m.time;
+ e.level = fromSpdlog(m.level);
+ e.message = std::string(m.payload);
+
+ spdlog::memory_buf_t formatted;
+ base_sink::formatter_->format(m, formatted);
+
+ if (formatted.size() >= 2) {
+ // remove \r\n
+ e.formattedMessage.assign(formatted.begin(), formatted.end() - 2);
+ } else {
+ e.formattedMessage = std::string(formatted);
+ }
+
+ (*m_f)(std::move(e));
+ } catch (std::exception& e) {
+ fprintf(stderr, "uncaugh exception in logging callback, %s\n", e.what());
+ } catch (...) {
+ fprintf(stderr, "uncaught exception in logging callback\n");
+ }
+ }
+
+ void flush_() override
+ {
+ // no-op
+ }
+
+private:
+ std::atomic<Callback*> m_f;
+};
+
+File::File() : type(None), maxSize(0), maxFiles(0), dailyHour(0), dailyMinute(0) {}
+
+File File::daily(fs::path file, int hour, int minute)
+{
+ File fl;
+
+ fl.type = Daily;
+ fl.file = std::move(file);
+ fl.dailyHour = hour;
+ fl.dailyMinute = minute;
+
+ return fl;
+}
+
+File File::rotating(fs::path file, std::size_t maxSize, std::size_t maxFiles)
+{
+ File fl;
+
+ fl.type = Rotating;
+ fl.file = std::move(file);
+ fl.maxSize = maxSize;
+ fl.maxFiles = maxFiles;
+
+ return fl;
+}
+
+File File::single(std::filesystem::path file)
+{
+ File fl;
+
+ fl.type = Single;
+ fl.file = std::move(file);
+
+ return fl;
+}
+
+spdlog::sink_ptr createFileSink(const File& f)
+{
+ try {
+ switch (f.type) {
+ case File::Daily: {
+ return std::make_shared<spdlog::sinks::daily_file_sink_mt>(
+ f.file.native(), f.dailyHour, f.dailyMinute);
+ }
+
+ case File::Rotating: {
+ return std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
+ f.file.native(), f.maxSize, f.maxFiles);
+ }
+
+ case File::Single: {
+ return std::make_shared<spdlog::sinks::basic_file_sink_mt>(f.file.native(), true);
+ }
+
+ case File::None: // fall-through
+ default:
+ return {};
+ }
+ } catch (spdlog::spdlog_ex& e) {
+ std::cerr << "failed to create file log, " << e.what() << "\n";
+ return {};
+ }
+}
+
+Logger::Logger(LoggerConfiguration conf_moved) : m_conf(std::move(conf_moved))
+{
+ createLogger(m_conf.name);
+
+ const auto timeType =
+ m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local;
+
+ m_logger->set_level(toSpdlog(m_conf.maxLevel));
+ m_logger->set_pattern(m_conf.pattern, timeType);
+ m_logger->flush_on(spdlog::level::trace);
+}
+
+// anchor
+Logger::~Logger() = default;
+
+Levels Logger::level() const
+{
+ return fromSpdlog(m_logger->level());
+}
+
+void Logger::setLevel(Levels lv)
+{
+ m_logger->set_level(toSpdlog(lv));
+}
+
+void Logger::setPattern(const std::string& s)
+{
+ m_logger->set_pattern(s);
+}
+
+void Logger::setFile(const File& f)
+{
+
+ if (m_file) {
+ auto* ds = static_cast<spdlog::sinks::dist_sink<std::mutex>*>(m_sinks.get());
+ ds->remove_sink(m_file);
+ m_file = {};
+ }
+
+ if (f.type != File::None) {
+ try {
+ m_file = createFileSink(f);
+
+ if (m_file) {
+ addSink(m_file);
+ }
+ } catch (spdlog::spdlog_ex& e) {
+ error("{}", e.what());
+ }
+ }
+}
+
+void Logger::setCallback(Callback* f)
+{
+ if (m_callback) {
+ static_cast<CallbackSink*>(m_callback.get())->setCallback(f);
+ } else {
+ m_callback.reset(new CallbackSink(f));
+ addSink(m_callback);
+ }
+}
+
+void Logger::addToBlacklist(const std::string& filter, const std::string& replacement)
+{
+ if (filter.length() <= 0 || replacement.length() <= 0) {
+ // nothing to do
+ return;
+ }
+
+ bool present = false;
+ for (BlacklistEntry& e : m_conf.blacklist) {
+ if (iequals(e.filter, filter)) {
+ e.replacement = replacement;
+ present = true;
+ break;
+ }
+ }
+ if (!present) {
+ m_conf.blacklist.push_back(BlacklistEntry(filter, replacement));
+ }
+}
+
+void Logger::removeFromBlacklist(const std::string& filter)
+{
+ if (filter.length() <= 0) {
+ // nothing to do
+ return;
+ }
+
+ for (auto it = m_conf.blacklist.begin(); it != m_conf.blacklist.end();) {
+ if (iequals(it->filter, filter)) {
+ it = m_conf.blacklist.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+void Logger::resetBlacklist()
+{
+ m_conf.blacklist.clear();
+}
+
+void Logger::createLogger(const std::string& name)
+{
+ m_sinks.reset(new spdlog::sinks::dist_sink<std::mutex>);
+
+#ifdef _WIN32
+ DWORD console_mode;
+ if (::GetConsoleMode(::GetStdHandle(STD_ERROR_HANDLE), &console_mode) != 0) {
+ using sink_type = spdlog::sinks::wincolor_stderr_sink_mt;
+ m_console.reset(new sink_type);
+
+ if (auto* cs = dynamic_cast<sink_type*>(m_console.get())) {
+ cs->set_color(spdlog::level::info,
+ FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
+ cs->set_color(spdlog::level::debug,
+ FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
+ }
+
+ addSink(m_console);
+ }
+#else
+ // On Linux, use ANSI color stderr sink
+ m_console.reset(new spdlog::sinks::ansicolor_stderr_sink_mt);
+ addSink(m_console);
+#endif
+
+ m_logger.reset(new spdlog::logger(name, m_sinks));
+}
+
+void Logger::addSink(std::shared_ptr<spdlog::sinks::sink> sink)
+{
+ // this is called for both the file and callback sinks
+ //
+ // in createLogger(), the dist_sink that was just created will be given the
+ // pattern that was set in Logger::Logger(), and will pass it to its children;
+ // the log level is irrelevant in child sinks because dist_sink checks it
+ // itself
+ //
+ // the problem then is that dist_sink doesn't have children yet, they're added
+ // in setFile() and setCallback(), which can be called by the user much later
+ // (or not at all)
+ //
+ // however, when a sink is added to dist_sink, it does _not_ set the pattern
+ // on it, it merely adds it to the list
+ //
+ // this sets the formatter on the sink manually before adding it to dist_sink
+
+ auto* ds = static_cast<spdlog::sinks::dist_sink<std::mutex>*>(m_sinks.get());
+
+ const auto timeType =
+ m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local;
+
+ sink->set_formatter(
+ std::make_unique<spdlog::pattern_formatter>(m_conf.pattern, timeType));
+
+ ds->add_sink(sink);
+}
+
+QString levelToString(Levels level)
+{
+ const auto spdlogLevel = toSpdlog(level);
+ const auto sv = spdlog::level::to_string_view(spdlogLevel);
+ const std::string s(sv.begin(), sv.end());
+
+ return QString::fromStdString(s);
+}
+
+void createDefault(LoggerConfiguration conf)
+{
+ g_default = std::make_unique<Logger>(conf);
+}
+
+Logger& getDefault()
+{
+ Q_ASSERT(g_default);
+ return *g_default;
+}
+
+} // namespace MOBase::log
+
+namespace MOBase::log::details
+{
+
+void doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept
+{
+ try {
+ const char* start = s.c_str();
+ const char* p = start;
+
+ for (;;) {
+ while (*p && *p != '\n') {
+ ++p;
+ }
+
+ std::string_view sv(start, static_cast<std::size_t>(p - start));
+ lg.log(toSpdlog(lv), "{}", sv);
+
+ if (!*p) {
+ break;
+ }
+
+ ++p;
+ start = p;
+ }
+ } catch (...) {
+ // eat it
+ }
+}
+
+} // namespace MOBase::log::details
diff --git a/libs/uibase/src/modrepositoryfileinfo.cpp b/libs/uibase/src/modrepositoryfileinfo.cpp
new file mode 100644
index 0000000..2b0f729
--- /dev/null
+++ b/libs/uibase/src/modrepositoryfileinfo.cpp
@@ -0,0 +1,81 @@
+#include <uibase/modrepositoryfileinfo.h>
+#include <uibase/json.h>
+
+MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo(
+ const ModRepositoryFileInfo& reference)
+ : QObject(reference.parent()), name(reference.name), uri(reference.uri),
+ description(reference.description), version(reference.version),
+ categoryID(reference.categoryID), modName(reference.modName),
+ gameName(reference.gameName), modID(reference.modID), fileID(reference.fileID),
+ fileSize(reference.fileSize), fileCategory(reference.fileCategory),
+ repository(reference.repository), userData(reference.userData),
+ author(reference.author), uploader(reference.uploader),
+ uploaderUrl(reference.uploaderUrl)
+{}
+
+MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo(QString gameName, int modID,
+ int fileID)
+ : name(), uri(), description(), version(), categoryID(0), modName(),
+ gameName(gameName), modID(modID), fileID(fileID), fileSize(0),
+ fileCategory(TYPE_UNKNOWN), repository(), userData(), author(), uploader(),
+ uploaderUrl()
+
+{}
+
+MOBase::ModRepositoryFileInfo
+MOBase::ModRepositoryFileInfo::createFromJson(const QString& data)
+{
+ QVariantList result = QtJson::parse(data).toList();
+
+ while (result.length() < 18) {
+ result.append(QVariant());
+ }
+
+ ModRepositoryFileInfo newInfo;
+
+ newInfo.gameName = result.at(0).toString();
+ newInfo.fileID = result.at(1).toInt();
+ newInfo.name = result.at(2).toString();
+ newInfo.uri = result.at(3).toString();
+ newInfo.version.parse(result.at(4).toString());
+ newInfo.description = result.at(5).toString();
+ newInfo.categoryID = result.at(6).toInt();
+ newInfo.fileSize = result.at(7).toUInt();
+ newInfo.modID = result.at(8).toInt();
+ newInfo.modName = result.at(9).toString();
+ newInfo.newestVersion.parse(result.at(10).toString());
+ newInfo.fileName = result.at(11).toString();
+ newInfo.fileCategory = result.at(12).toInt();
+ newInfo.repository = result.at(13).toString();
+ newInfo.userData = result.at(14).toMap();
+ newInfo.author = result.at(15).toString();
+ newInfo.uploader = result.at(16).toString();
+ newInfo.uploaderUrl = result.at(17).toString();
+
+ return newInfo;
+}
+
+QString MOBase::ModRepositoryFileInfo::toString() const
+{
+ return QString("[ "
+ "\"%1\",%2,\"%3\",\"%4\",\"%5\",\"%6\",%7,%8,%9,\"%10\",\"%11\",\"%"
+ "12\",%13,\"%14\",%15 ]")
+ .arg(gameName)
+ .arg(fileID)
+ .arg(name)
+ .arg(uri)
+ .arg(version.canonicalString())
+ .arg(description.mid(0).replace("\"", "'"))
+ .arg(categoryID)
+ .arg(fileSize)
+ .arg(modID)
+ .arg(modName)
+ .arg(newestVersion.canonicalString())
+ .arg(fileName)
+ .arg(fileCategory)
+ .arg(repository)
+ .arg(QString(QtJson::serialize(userData)))
+ .arg(author)
+ .arg(uploader)
+ .arg(uploaderUrl);
+}
diff --git a/libs/uibase/src/nxmurl.cpp b/libs/uibase/src/nxmurl.cpp
new file mode 100644
index 0000000..d09cdde
--- /dev/null
+++ b/libs/uibase/src/nxmurl.cpp
@@ -0,0 +1,44 @@
+/*
+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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/nxmurl.h>
+#include <uibase/utility.h>
+#include <QList>
+#include <QRegularExpression>
+#include <QString>
+#include <QUrl>
+#include <QUrlQuery>
+
+NXMUrl::NXMUrl(const QString& url)
+{
+ QUrl nxm(url);
+ QUrlQuery query(nxm);
+ QRegularExpression exp("nxm://[a-z0-9]+/mods/(\\d+)/files/(\\d+)",
+ QRegularExpression::CaseInsensitiveOption);
+ auto match = exp.match(url);
+ if (!match.hasMatch()) {
+ throw MOBase::InvalidNXMLinkException(url);
+ }
+ m_Game = nxm.host();
+ m_ModId = match.captured(1).toInt();
+ m_FileId = match.captured(2).toInt();
+ m_Key = query.queryItemValue("key");
+ m_Expires = query.queryItemValue("expires").toInt();
+ m_UserId = query.queryItemValue("user_id").toInt();
+}
diff --git a/libs/uibase/src/pch.cpp b/libs/uibase/src/pch.cpp
new file mode 100644
index 0000000..1d9f38c
--- /dev/null
+++ b/libs/uibase/src/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/libs/uibase/src/pch.h b/libs/uibase/src/pch.h
new file mode 100644
index 0000000..bd7e3bc
--- /dev/null
+++ b/libs/uibase/src/pch.h
@@ -0,0 +1,139 @@
+#ifdef _MSC_VER
+#pragma warning(disable : 4251) // neds to have dll-interface
+#pragma warning(disable : 4355) // this used in initializer list
+#pragma warning(disable : 4371) // layout may have changed
+#pragma warning(disable : 4514) // unreferenced inline function removed
+#pragma warning(disable : 4571) // catch semantics changed
+#pragma warning(disable : 4619) // no warning X
+#pragma warning(disable : 4623) // default constructor deleted
+#pragma warning(disable : 4625) // copy constructor deleted
+#pragma warning(disable : 4626) // copy assignment operator deleted
+#pragma warning(disable : 4710) // function not inlined
+#pragma warning(disable : 4820) // padding
+#pragma warning(disable : 4866) // left-to-right evaluation order
+#pragma warning(disable : 4868) // left-to-right evaluation order
+#pragma warning(disable : 5026) // move constructor deleted
+#pragma warning(disable : 5027) // move assignment operator deleted
+#pragma warning(disable : 5045) // spectre mitigation
+
+#pragma warning(push, 3)
+#pragma warning(disable : 4242) // uint -> const quint8
+#pragma warning(disable : 4365) // signed/unsigned mismatch
+#pragma warning(disable : 4668) // preprocessor macro used but not defined
+#pragma warning(disable : 4774) // bad format string
+#pragma warning(disable : 4946) // reinterpret_cast used between related classes
+#pragma warning(disable : 4800) // implicit conversion
+#pragma warning(disable : 5219) // implicit int -> float conversion
+#pragma warning( \
+ disable : 5249) // named enumerators with values outside of bit field width
+#endif // _MSC_VER
+
+#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING 1
+
+// std
+#include <algorithm>
+#include <cstdint>
+#include <cwctype>
+#include <filesystem>
+#include <functional>
+#include <iostream>
+#include <list>
+#include <map>
+#include <memory>
+#include <set>
+#include <sstream>
+#include <stdlib.h>
+#include <string>
+#include <typeindex>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+#include <wchar.h>
+
+// windows
+#ifdef _WIN32
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+#ifndef WIN32_MEAN_AND_LEAN
+#define WIN32_MEAN_AND_LEAN
+#endif
+#include <ShlObj.h>
+#include <Windows.h>
+#include <shobjidl.h>
+#endif // _WIN32
+
+// Qt
+#include <QAbstractButton>
+#include <QAction>
+#include <QApplication>
+#include <QBitmap>
+#include <QBuffer>
+#include <QCommandLinkButton>
+#include <QCoreApplication>
+#include <QCryptographicHash>
+#include <QDateTime>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QDir>
+#include <QDropEvent>
+#include <QFile>
+#include <QFileInfo>
+#include <QFlags>
+#include <QGraphicsObject>
+#include <QIcon>
+#include <QImage>
+#include <QLabel>
+#include <QLineEdit>
+#include <QList>
+#include <QMainWindow>
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QMetaEnum>
+#include <QMetaMethod>
+#include <QMetaObject>
+#include <QMetaType>
+#include <QMouseEvent>
+#include <QMutex>
+#include <QMutexLocker>
+#include <QNetworkReply>
+#include <QObject>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QQuickItem>
+#include <QQuickWidget>
+#include <QRect>
+#include <QRegularExpression>
+#include <QResizeEvent>
+#include <QScreen>
+#include <QSettings>
+#include <QShortcutEvent>
+#include <QShowEvent>
+#include <QString>
+#include <QStyle>
+#include <QTabWidget>
+#include <QTableWidget>
+#include <QTemporaryFile>
+#include <QTextEdit>
+#include <QTextStream>
+#include <QTime>
+#include <QTimer>
+#include <QToolBar>
+#include <QToolButton>
+#include <QTreeWidget>
+#include <QUrl>
+#include <QUrlQuery>
+#include <QVBoxLayout>
+#include <QVariant>
+#include <QVariantMap>
+#include <QVersionNumber>
+#include <QWidget>
+#include <QtDebug>
+#include <qmetaobject.h>
+
+#undef _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
diff --git a/libs/uibase/src/pluginrequirements.cpp b/libs/uibase/src/pluginrequirements.cpp
new file mode 100644
index 0000000..a2a11ef
--- /dev/null
+++ b/libs/uibase/src/pluginrequirements.cpp
@@ -0,0 +1,138 @@
+#include <uibase/pluginrequirements.h>
+
+#include <QCoreApplication>
+
+#include <uibase/imoinfo.h>
+#include <uibase/iplugindiagnose.h>
+#include <uibase/iplugingame.h>
+
+using namespace MOBase;
+
+// Plugin and Game dependencies
+
+PluginDependencyRequirement::PluginDependencyRequirement(QStringList const& pluginNames)
+ : m_PluginNames(pluginNames)
+{}
+
+std::optional<IPluginRequirement::Problem>
+PluginDependencyRequirement::check(IOrganizer* o) const
+{
+ for (auto const& pluginName : m_PluginNames) {
+ if (o->isPluginEnabled(pluginName)) {
+ return {};
+ }
+ }
+ return Problem(message());
+}
+
+QString PluginDependencyRequirement::message() const
+{
+ if (m_PluginNames.size() > 1) {
+ return QObject::tr("One of the following plugins must be enabled: %1.")
+ .arg(m_PluginNames.join(", "));
+ } else {
+ return QObject::tr("This plugin can only be enabled if the '%1' plugin is "
+ "installed and enabled.")
+ .arg(m_PluginNames[0]);
+ }
+}
+
+GameDependencyRequirement::GameDependencyRequirement(QStringList const& gameNames)
+ : m_GameNames(gameNames)
+{}
+
+std::optional<IPluginRequirement::Problem>
+GameDependencyRequirement::check(IOrganizer* o) const
+{
+ auto* game = o->managedGame();
+ if (!game) {
+ return Problem(message());
+ }
+
+ QString gameName = game->gameName();
+ for (auto const& pluginName : m_GameNames) {
+ if (pluginName.compare(gameName, Qt::CaseInsensitive) == 0) {
+ return {};
+ }
+ }
+ return Problem(message());
+}
+
+QString GameDependencyRequirement::message() const
+{
+ return QObject::tr("This plugin can only be enabled for the following game(s): %1.",
+ "", static_cast<int>(m_GameNames.size()))
+ .arg(m_GameNames.join(", "));
+}
+
+// Diagnose requirements
+
+DiagnoseRequirement::DiagnoseRequirement(const IPluginDiagnose* diagnose)
+ : m_Diagnose(diagnose)
+{}
+
+std::optional<IPluginRequirement::Problem> DiagnoseRequirement::check(IOrganizer*) const
+{
+ auto activeProblems = m_Diagnose->activeProblems();
+
+ if (activeProblems.empty()) {
+ return {};
+ }
+
+ QStringList shortDescriptions, longDescriptions;
+ for (auto i : activeProblems) {
+ shortDescriptions.append(m_Diagnose->shortDescription(i));
+ longDescriptions.append(m_Diagnose->fullDescription(i));
+ }
+
+ return Problem(shortDescriptions.join("\n"), longDescriptions.join("\n"));
+}
+
+// Basic requirements
+class BasicPluginRequirement : public IPluginRequirement
+{
+public:
+ BasicPluginRequirement(std::function<bool(IOrganizer*)> const& checker,
+ QString const description)
+ : m_Checker(checker), m_Description(description)
+ {}
+
+ std::optional<Problem> check(IOrganizer* o) const
+ {
+ if (m_Checker(o)) {
+ return {};
+ }
+ return Problem(m_Description);
+ }
+
+private:
+ std::function<bool(IOrganizer*)> m_Checker;
+ QString m_Description;
+};
+
+// Factory
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::pluginDependency(QStringList const& pluginNames)
+{
+ return std::make_shared<PluginDependencyRequirement>(pluginNames);
+}
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::gameDependency(QStringList const& pluginGameNames)
+{
+ return std::make_shared<GameDependencyRequirement>(pluginGameNames);
+}
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::diagnose(const IPluginDiagnose* diagnose)
+{
+ return std::make_shared<DiagnoseRequirement>(diagnose);
+}
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::basic(std::function<bool(IOrganizer*)> const& checker,
+ QString const description)
+{
+ return std::make_shared<BasicPluginRequirement>(checker, description);
+}
diff --git a/libs/uibase/src/questionboxmemory.cpp b/libs/uibase/src/questionboxmemory.cpp
new file mode 100644
index 0000000..586e52e
--- /dev/null
+++ b/libs/uibase/src/questionboxmemory.cpp
@@ -0,0 +1,200 @@
+/*
+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
+*/
+
+#include <uibase/questionboxmemory.h>
+#include <uibase/log.h>
+#include "ui_questionboxmemory.h"
+
+#include <QApplication>
+#include <QIcon>
+#include <QMutex>
+#include <QMutexLocker>
+#include <QPushButton>
+#include <QSettings>
+#include <QStyle>
+
+namespace MOBase
+{
+
+static QMutex g_mutex;
+static QuestionBoxMemory::GetButton g_get;
+static QuestionBoxMemory::SetWindowButton g_setWindow;
+static QuestionBoxMemory::SetFileButton g_setFile;
+
+QuestionBoxMemory::QuestionBoxMemory(QWidget* parent, const QString& title,
+ const QString& text, QString const* filename,
+ const QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+ : QDialog(parent), ui(new Ui::QuestionBoxMemory), m_Button(QDialogButtonBox::Cancel)
+{
+ ui->setupUi(this);
+
+ setWindowFlag(Qt::WindowType::WindowContextHelpButtonHint, false);
+ setWindowTitle(title);
+
+ QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion);
+ ui->iconLabel->setPixmap(icon.pixmap(128));
+ ui->messageLabel->setText(text);
+
+ if (filename == nullptr) {
+ // delete the 2nd check box
+ QCheckBox* box = ui->rememberForCheckBox;
+ box->parentWidget()->layout()->removeWidget(box);
+ delete box;
+ } else {
+ ui->rememberForCheckBox->setText(ui->rememberForCheckBox->text().arg(*filename));
+ }
+
+ ui->buttonBox->setStandardButtons(buttons);
+
+ if (defaultButton != QDialogButtonBox::NoButton) {
+ ui->buttonBox->button(defaultButton)->setDefault(true);
+ }
+
+ connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this,
+ SLOT(buttonClicked(QAbstractButton*)));
+}
+
+QuestionBoxMemory::~QuestionBoxMemory() = default;
+
+void QuestionBoxMemory::setCallbacks(GetButton get, SetWindowButton setWindow,
+ SetFileButton setFile)
+{
+ QMutexLocker locker(&g_mutex);
+
+ g_get = get;
+ g_setWindow = setWindow;
+ g_setFile = setFile;
+}
+
+void QuestionBoxMemory::buttonClicked(QAbstractButton* button)
+{
+ m_Button = ui->buttonBox->standardButton(button);
+}
+
+QDialogButtonBox::StandardButton
+QuestionBoxMemory::query(QWidget* parent, const QString& windowName,
+ const QString& title, const QString& text,
+ QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+{
+ return queryImpl(parent, windowName, nullptr, title, text, buttons, defaultButton);
+}
+
+QDialogButtonBox::StandardButton
+QuestionBoxMemory::query(QWidget* parent, const QString& windowName,
+ const QString& fileName, const QString& title,
+ const QString& text, QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+{
+ return queryImpl(parent, windowName, &fileName, title, text, buttons, defaultButton);
+}
+
+QDialogButtonBox::StandardButton
+QuestionBoxMemory::queryImpl(QWidget* parent, const QString& windowName,
+ const QString* fileName, const QString& title,
+ const QString& text,
+ QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+{
+ QMutexLocker locker(&g_mutex);
+
+ const auto button = getMemory(windowName, (fileName ? *fileName : ""));
+ if (button != NoButton) {
+ log::debug("{}: not asking because user always wants response {}",
+ windowName + (fileName ? QString("/") + *fileName : ""),
+ buttonToString(button));
+
+ return button;
+ }
+
+ QuestionBoxMemory dialog(parent, title, text, fileName, buttons, defaultButton);
+ dialog.exec();
+
+ if (dialog.m_Button != QDialogButtonBox::Cancel) {
+ if (dialog.ui->rememberCheckBox->isChecked()) {
+ setWindowMemory(windowName, dialog.m_Button);
+ }
+
+ if (fileName != nullptr && dialog.ui->rememberForCheckBox->isChecked()) {
+ setFileMemory(windowName, *fileName, dialog.m_Button);
+ }
+ }
+
+ return dialog.m_Button;
+}
+
+void QuestionBoxMemory::setWindowMemory(const QString& windowName, Button b)
+{
+ log::debug("remembering choice {} for window {}", buttonToString(b), windowName);
+
+ g_setWindow(windowName, b);
+}
+
+void QuestionBoxMemory::setFileMemory(const QString& windowName,
+ const QString& filename, Button b)
+{
+ log::debug("remembering choice {} for file {}", buttonToString(b),
+ windowName + "/" + filename);
+
+ g_setFile(windowName, filename, b);
+}
+
+QuestionBoxMemory::Button QuestionBoxMemory::getMemory(const QString& windowName,
+ const QString& filename)
+{
+ return g_get(windowName, filename);
+}
+
+QString QuestionBoxMemory::buttonToString(Button b)
+{
+ using BB = QDialogButtonBox;
+
+ static const std::map<Button, QString> map = {
+ {BB::NoButton, "none"},
+ {BB::Ok, "ok"},
+ {BB::Save, "save"},
+ {BB::SaveAll, "saveall"},
+ {BB::Open, "open"},
+ {BB::Yes, "yes"},
+ {BB::YesToAll, "yestoall"},
+ {BB::No, "no"},
+ {BB::NoToAll, "notoall"},
+ {BB::Abort, "abort"},
+ {BB::Retry, "retry"},
+ {BB::Ignore, "ignore"},
+ {BB::Close, "close"},
+ {BB::Cancel, "cancel"},
+ {BB::Discard, "discard"},
+ {BB::Help, "help"},
+ {BB::Apply, "apply"},
+ {BB::Reset, "reset"},
+ {BB::RestoreDefaults, "restoredefaults"}};
+
+ auto itor = map.find(b);
+
+ if (itor == map.end()) {
+ return QString("0x%1").arg(static_cast<int>(b), 0, 16);
+ } else {
+ return QString("'%1' (0x%2)").arg(itor->second).arg(static_cast<int>(b), 0, 16);
+ }
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/questionboxmemory.ui b/libs/uibase/src/questionboxmemory.ui
new file mode 100644
index 0000000..8237571
--- /dev/null
+++ b/libs/uibase/src/questionboxmemory.ui
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QuestionBoxMemory</class>
+ <widget class="QDialog" name="QuestionBoxMemory">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>374</width>
+ <height>130</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">Placeholder</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
+ <property name="spacing">
+ <number>1</number>
+ </property>
+ <property name="leftMargin">
+ <number>2</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QFrame" name="frame">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0">
+ <item>
+ <widget class="QLabel" name="iconLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>36</width>
+ <height>36</height>
+ </size>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="messageLabel">
+ <property name="font">
+ <font>
+ <pointsize>9</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="leftMargin">
+ <number>7</number>
+ </property>
+ <property name="topMargin">
+ <number>7</number>
+ </property>
+ <property name="rightMargin">
+ <number>7</number>
+ </property>
+ <property name="bottomMargin">
+ <number>7</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="rememberCheckBox">
+ <property name="text">
+ <string>Remember selection</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="rememberForCheckBox">
+ <property name="text">
+ <string>Remember selection only for %1</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::No|QDialogButtonBox::Yes</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>QuestionBoxMemory</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>QuestionBoxMemory</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/libs/uibase/src/registry.cpp b/libs/uibase/src/registry.cpp
new file mode 100644
index 0000000..0b6834f
--- /dev/null
+++ b/libs/uibase/src/registry.cpp
@@ -0,0 +1,189 @@
+/*
+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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/registry.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <QApplication>
+#include <QFile>
+#include <QFileInfo>
+#include <QList>
+#include <QMessageBox>
+#include <QString>
+#include <QTextStream>
+
+namespace MOBase
+{
+
+// Line-by-line INI writer that preserves the file format.
+// Unlike QSettings::IniFormat, this does NOT interpret backslashes as
+// line continuations, does NOT URL-encode spaces in key names, and does
+// NOT reorder keys. It only modifies the target key=value pair and
+// leaves everything else untouched.
+static bool writeIniValueDirect(const QString& section, const QString& key,
+ const QString& value, const QString& fileName)
+{
+ QStringList lines;
+ bool fileExists = QFileInfo::exists(fileName);
+
+ if (fileExists) {
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return false;
+ }
+ QTextStream in(&file);
+ while (!in.atEnd()) {
+ lines.append(in.readLine());
+ }
+ file.close();
+ }
+
+ // Find the target section and key
+ QString sectionHeader = "[" + section + "]";
+ int sectionStart = -1;
+ int sectionEnd = lines.size(); // end of file if section is last
+ int keyLine = -1;
+
+ for (int i = 0; i < lines.size(); ++i) {
+ QString trimmed = lines[i].trimmed();
+ if (trimmed.compare(sectionHeader, Qt::CaseInsensitive) == 0) {
+ sectionStart = i;
+ // Find end of this section (next section header or EOF)
+ for (int j = i + 1; j < lines.size(); ++j) {
+ QString t = lines[j].trimmed();
+ if (t.startsWith('[') && t.endsWith(']')) {
+ sectionEnd = j;
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ if (sectionStart >= 0) {
+ // Section found, look for the key within it
+ for (int i = sectionStart + 1; i < sectionEnd; ++i) {
+ QString trimmed = lines[i].trimmed();
+ // Skip comments and empty lines
+ if (trimmed.isEmpty() || trimmed.startsWith(';') || trimmed.startsWith('#')) {
+ continue;
+ }
+ int eqPos = trimmed.indexOf('=');
+ if (eqPos > 0) {
+ QString existingKey = trimmed.left(eqPos).trimmed();
+ if (existingKey.compare(key, Qt::CaseInsensitive) == 0) {
+ keyLine = i;
+ break;
+ }
+ }
+ }
+
+ if (keyLine >= 0) {
+ // Key found, replace the line preserving indentation
+ QString original = lines[keyLine];
+ int eqPos = original.indexOf('=');
+ // Preserve everything up to and including '='
+ lines[keyLine] = original.left(eqPos + 1) + value;
+ } else {
+ // Key not found in section, insert it after the section header
+ lines.insert(sectionStart + 1, key + "=" + value);
+ }
+ } else {
+ // Section not found, append it
+ if (!lines.isEmpty() && !lines.last().trimmed().isEmpty()) {
+ lines.append(""); // blank line before new section
+ }
+ lines.append(sectionHeader);
+ lines.append(key + "=" + value);
+ }
+
+ // Write back
+ QFile outFile(fileName);
+ if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ return false;
+ }
+ QTextStream out(&outFile);
+ for (int i = 0; i < lines.size(); ++i) {
+ out << lines[i];
+ if (i < lines.size() - 1) {
+ out << '\n';
+ }
+ }
+ // Preserve trailing newline if original had one, or add one
+ out << '\n';
+ outFile.close();
+
+ return true;
+}
+
+bool WriteRegistryValue(const QString& appName, const QString& keyName,
+ const QString& value, const QString& fileName)
+{
+ if (writeIniValueDirect(appName, keyName, value, fileName)) {
+ return true;
+ }
+
+ // Write failed, check if the file is read-only
+ QFileInfo fileInfo(fileName);
+
+ QMessageBox::StandardButton result =
+ MOBase::TaskDialog(qApp->activeModalWidget(),
+ QObject::tr("INI file is read-only"))
+ .main(QObject::tr("INI file is read-only"))
+ .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" "
+ "which is currently set to read-only.")
+ .arg(fileInfo.fileName()))
+ .icon(QMessageBox::Warning)
+ .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes})
+ .button({QObject::tr("Allow the write once"),
+ QObject::tr("The file will be set to read-only again."),
+ QMessageBox::Ignore})
+ .button({QObject::tr("Skip this file"), QMessageBox::No})
+ .remember("clearReadOnly", fileInfo.fileName())
+ .exec();
+
+ if (result & (QMessageBox::Yes | QMessageBox::Ignore)) {
+ // Make the file writable
+ QFile file(fileName);
+ file.setPermissions(file.permissions() | QFile::WriteUser | QFile::WriteOwner);
+
+ bool ok = writeIniValueDirect(appName, keyName, value, fileName);
+
+ if (result == QMessageBox::Ignore) {
+ // Set back to read-only
+ file.setPermissions(file.permissions() & ~(QFile::WriteUser | QFile::WriteOwner));
+ }
+
+ return ok;
+ }
+
+ return false;
+}
+
+#ifdef _WIN32
+bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName,
+ const wchar_t* value, const wchar_t* fileName)
+{
+ return WriteRegistryValue(
+ QString::fromWCharArray(appName),
+ QString::fromWCharArray(keyName),
+ QString::fromWCharArray(value),
+ QString::fromWCharArray(fileName));
+}
+#endif
+
+} // namespace MOBase
diff --git a/libs/uibase/src/report.cpp b/libs/uibase/src/report.cpp
new file mode 100644
index 0000000..1c47d95
--- /dev/null
+++ b/libs/uibase/src/report.cpp
@@ -0,0 +1,453 @@
+/*
+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
+*/
+
+#include <uibase/report.h>
+#include <uibase/expanderwidget.h>
+#include <uibase/log.h>
+#include <uibase/questionboxmemory.h>
+#include "ui_taskdialog.h"
+#include <uibase/utility.h>
+#include <QComboBox>
+#include <QMainWindow>
+#include <QRadioButton>
+#include <QThread>
+
+namespace MOBase
+{
+
+QWidget* topLevelWindow()
+{
+ if (!qApp) {
+ return nullptr;
+ }
+
+ for (QWidget* w : qApp->topLevelWidgets()) {
+ if (dynamic_cast<QMainWindow*>(w)) {
+ return w;
+ }
+ }
+
+ return nullptr;
+}
+
+void criticalOnTop(const QString& message)
+{
+ QMessageBox mb(QMessageBox::Critical, "Mod Organizer", message);
+
+ mb.show();
+ mb.activateWindow();
+ mb.raise();
+ mb.exec();
+}
+
+void reportError(const QString& message)
+{
+ log::error("{}", message);
+
+ // QMessageBox must only be created on the main (GUI) thread.
+ // If we're on a background thread, just log — showing a dialog would crash.
+ if (QThread::currentThread() != qApp->thread()) {
+ return;
+ }
+
+ if (QApplication::topLevelWidgets().count() != 0) {
+ if (auto* mw = topLevelWindow()) {
+ QMessageBox::warning(mw, QObject::tr("Error"), message, QMessageBox::Ok);
+ } else {
+ criticalOnTop(message);
+ }
+ } else {
+ // No top-level widget available, show a standalone critical dialog
+ criticalOnTop(message);
+ }
+}
+
+TaskDialogButton::TaskDialogButton(QString t, QString d, QMessageBox::StandardButton b)
+ : text(std::move(t)), description(std::move(d)), button(b)
+{}
+
+TaskDialogButton::TaskDialogButton(QString t, QMessageBox::StandardButton b)
+ : TaskDialogButton(std::move(t), {}, b)
+{}
+
+TaskDialog::TaskDialog(QWidget* parent, QString title)
+ : m_dialog(new QDialog(parent)), ui(new Ui::TaskDialog), m_title(std::move(title)),
+ m_icon(QMessageBox::NoIcon), m_result(QMessageBox::Cancel), m_width(-1),
+ m_rememberCheck(nullptr), m_rememberCombo(nullptr)
+{
+ ui->setupUi(m_dialog.get());
+}
+
+TaskDialog::~TaskDialog() = default;
+
+TaskDialog& TaskDialog::title(const QString& s)
+{
+ m_title = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::main(const QString& s)
+{
+ m_main = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::content(const QString& s)
+{
+ m_content = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::details(const QString& s)
+{
+ m_details = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::icon(QMessageBox::Icon i)
+{
+ m_icon = i;
+ return *this;
+}
+
+TaskDialog& TaskDialog::button(TaskDialogButton b)
+{
+ m_buttons.emplace_back(std::move(b));
+ return *this;
+}
+
+TaskDialog& TaskDialog::remember(const QString& action, const QString& file)
+{
+ m_rememberAction = action;
+ m_rememberFile = file;
+ return *this;
+}
+
+void TaskDialog::addContent(QWidget* w)
+{
+ auto* ly = static_cast<QVBoxLayout*>(ui->contentPanel->layout());
+
+ // add before spacer
+ ly->insertWidget(ly->count() - 1, w);
+}
+
+void TaskDialog::setWidth(int w)
+{
+ m_width = w;
+}
+
+QMessageBox::StandardButton TaskDialog::exec()
+{
+ const auto b = checkMemory();
+ if (b != QMessageBox::NoButton) {
+ return b;
+ }
+
+ setDialog();
+ setWidgets();
+ setChoices();
+ setButtons();
+ setDetails();
+
+ if (m_width >= 0) {
+ ui->topPanel->setMinimumWidth(m_width);
+ } else {
+ ui->topPanel->setMinimumWidth(400);
+ }
+
+ m_dialog->adjustSize();
+
+ if (!m_dialog->parent()) {
+ // no parent, make sure it's shown on top
+ m_dialog->show();
+ m_dialog->activateWindow();
+ m_dialog->raise();
+ }
+
+ if (m_dialog->exec() != QDialog::Accepted) {
+ return QMessageBox::Cancel;
+ }
+
+ rememberChoice();
+ return m_result;
+}
+
+QMessageBox::StandardButton TaskDialog::checkMemory() const
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ return QMessageBox::NoButton;
+ }
+
+ const auto b = QuestionBoxMemory::getMemory(m_rememberAction, m_rememberFile);
+
+ const auto logName = m_rememberAction +
+ (m_rememberFile.isEmpty() ? "" : QString("/") + m_rememberFile);
+
+ if (b == QDialogButtonBox::NoButton) {
+ log::debug("{}: asking because the user has not set a choice before", logName);
+ } else {
+ log::debug("{}: not asking because user always wants response {}", logName,
+ QuestionBoxMemory::buttonToString(b));
+ }
+
+ return static_cast<QMessageBox::StandardButton>(b);
+}
+
+void TaskDialog::rememberChoice()
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ // nothing
+ return;
+ }
+
+ const auto b = static_cast<QuestionBoxMemory::Button>(m_result);
+
+ if (m_rememberCheck) {
+ // action only
+ if (m_rememberCheck->isChecked()) {
+ QuestionBoxMemory::setWindowMemory(m_rememberAction, b);
+ }
+ } else if (m_rememberCombo) {
+ Q_ASSERT(m_rememberCombo->count() == 3);
+
+ if (m_rememberCombo->currentIndex() == 1) {
+ // remember action
+ QuestionBoxMemory::setWindowMemory(m_rememberAction, b);
+ } else if (m_rememberCombo->currentIndex() == 2) {
+ // remember file
+ QuestionBoxMemory::setFileMemory(m_rememberAction, m_rememberFile, b);
+ }
+ }
+}
+
+void TaskDialog::setButtons()
+{
+ if (m_buttons.empty()) {
+ setStandardButtons();
+ } else {
+ setCommandButtons();
+ }
+
+ m_expander.reset(new ExpanderWidget(ui->detailsExpander, ui->detailsWidget));
+}
+
+void TaskDialog::setStandardButtons()
+{
+ ui->standardButtonsPanel->show();
+ ui->commandButtonsPanel->hide();
+
+ for (auto* b : ui->standardButtons->buttons()) {
+ ui->standardButtons->removeButton(b);
+ }
+
+ ui->standardButtons->addButton(QDialogButtonBox::Ok);
+
+ QObject::connect(ui->standardButtons, &QDialogButtonBox::clicked, [&](auto* b) {
+ m_result = static_cast<QMessageBox::StandardButton>(
+ ui->standardButtons->standardButton(b));
+
+ m_dialog->accept();
+ });
+}
+
+void TaskDialog::setCommandButtons()
+{
+ ui->standardButtonsPanel->hide();
+ ui->commandButtonsPanel->show();
+
+ deleteChildWidgets(ui->commandButtons);
+
+ for (auto&& b : m_buttons) {
+ auto* cb = new QCommandLinkButton(b.text, b.description);
+
+ QObject::connect(cb, &QAbstractButton::clicked, [&] {
+ m_result = b.button;
+ m_dialog->accept();
+ });
+
+ ui->commandButtons->layout()->addWidget(cb);
+ }
+}
+
+void TaskDialog::setDetails()
+{
+ if (m_details.isEmpty()) {
+ ui->detailsExpander->setEnabled(false);
+ return;
+ }
+
+ ui->details->setPlainText(m_details);
+
+ setVisibleLines(ui->details, 10);
+ setFontPercent(ui->details, 0.9);
+
+ const QColor bg = detailsColor();
+
+ ui->detailsPanel->setStyleSheet(QString("background-color: rgb(%1, %2, %3)")
+ .arg(bg.redF() * 255)
+ .arg(bg.greenF() * 255)
+ .arg(bg.blueF() * 255));
+}
+
+void TaskDialog::setDialog()
+{
+#ifdef _WIN32
+ m_dialog->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
+#else
+ m_dialog->setWindowFlags(Qt::Dialog);
+#endif
+ m_dialog->layout()->setSizeConstraint(QLayout::SetFixedSize);
+
+ m_dialog->setWindowTitle(m_title);
+}
+
+void TaskDialog::setWidgets()
+{
+ ui->main->setText(m_main);
+ setFontPercent(ui->main, 1.5);
+
+ ui->content->setText(m_content);
+ ui->content->setVisible(!m_content.isEmpty());
+
+ auto icon = standardIcon(m_icon);
+
+ if (icon.isNull()) {
+ ui->iconPanel->hide();
+ } else {
+ ui->iconPanel->show();
+ ui->icon->setPixmap(std::move(icon));
+ }
+}
+
+void TaskDialog::setChoices()
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ ui->rememberPanel->hide();
+ return;
+ }
+
+ ui->rememberPanel->show();
+ deleteChildWidgets(ui->rememberPanel);
+
+ const auto tooltip = QObject::tr(
+ "You can reset these choices by clicking \"Reset Dialog Choices\" in the "
+ "General tab of the Settings");
+
+ if (!m_rememberAction.isEmpty() && !m_rememberFile.isEmpty()) {
+ // both
+ m_rememberCombo = new QComboBox;
+ m_rememberCombo->setToolTip(tooltip);
+
+ m_rememberCombo->addItem(QObject::tr("Always ask"));
+ m_rememberCombo->addItem(QObject::tr("Remember my choice"));
+ m_rememberCombo->addItem(
+ QObject::tr("Remember my choice for %1").arg(m_rememberFile));
+
+ ui->rememberPanel->layout()->setAlignment(Qt::AlignLeft);
+ ui->rememberPanel->layout()->addWidget(m_rememberCombo);
+ } else if (!m_rememberAction.isEmpty() || !m_rememberFile.isEmpty()) {
+ // either
+ m_rememberCheck = new QCheckBox(QObject::tr("Remember my choice"));
+ m_rememberCheck->setToolTip(tooltip);
+ ui->rememberPanel->layout()->addWidget(m_rememberCheck);
+ }
+}
+
+QColor TaskDialog::detailsColor() const
+{
+ auto b = std::make_unique<QPushButton>();
+ m_dialog->style()->polish(b.get());
+ return b->palette().color(b->backgroundRole());
+}
+
+QPixmap TaskDialog::standardIcon(QMessageBox::Icon icon) const
+{
+ QStyle* s = m_dialog->style();
+ QIcon i;
+
+ switch (icon) {
+ case QMessageBox::Information:
+ i = s->standardIcon(QStyle::SP_MessageBoxInformation, 0, m_dialog.get());
+ break;
+ case QMessageBox::Warning:
+ i = s->standardIcon(QStyle::SP_MessageBoxWarning, 0, m_dialog.get());
+ break;
+ case QMessageBox::Critical:
+ i = s->standardIcon(QStyle::SP_MessageBoxCritical, 0, m_dialog.get());
+ break;
+ case QMessageBox::Question:
+ i = s->standardIcon(QStyle::SP_MessageBoxQuestion, 0, m_dialog.get());
+ break;
+ case QMessageBox::NoIcon:
+ [[fallthrough]];
+ default:
+ break;
+ }
+
+ if (i.isNull()) {
+ return {};
+ }
+
+ QWindow* window = m_dialog->windowHandle();
+ if (!window) {
+ if (const QWidget* nativeParent = m_dialog->nativeParentWidget())
+ window = nativeParent->windowHandle();
+ }
+
+ const int iconSize = s->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, m_dialog.get());
+
+ return i.pixmap(QSize(iconSize, iconSize));
+}
+
+void TaskDialog::setVisibleLines(QPlainTextEdit* w, int lines)
+{
+ QTextDocument* d = w->document();
+ QFontMetrics fm(d->defaultFont());
+
+ const QMargins margins = ui->details->contentsMargins();
+
+ double height = 0;
+
+ // lines
+ height += fm.lineSpacing() * lines;
+
+ // top and bottom margins for document and frame
+ height += (d->documentMargin() + ui->details->frameWidth()) * 2;
+
+ // widget margins
+ height += margins.top() + margins.bottom();
+
+ w->setMaximumHeight(static_cast<int>(std::round(height)));
+}
+
+void TaskDialog::setFontPercent(QWidget* w, double p)
+{
+ auto f = w->font();
+
+ if (f.pointSizeF() > 0) {
+ f.setPointSizeF(f.pointSizeF() * p);
+ } else if (f.pixelSize() > 0) {
+ f.setPixelSize(static_cast<int>(std::round(f.pixelSize() * p)));
+ }
+
+ w->setFont(f);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/safewritefile.cpp b/libs/uibase/src/safewritefile.cpp
new file mode 100644
index 0000000..fb0819f
--- /dev/null
+++ b/libs/uibase/src/safewritefile.cpp
@@ -0,0 +1,84 @@
+/*
+Copyright (C) 2014 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 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/safewritefile.h>
+#include <uibase/log.h>
+#include <QStorageInfo>
+#include <QString>
+
+namespace MOBase
+{
+
+#ifdef _WIN32
+
+SafeWriteFile::SafeWriteFile(const QString& fileName) : m_SaveFile(fileName)
+{
+ if (!m_SaveFile.open(QIODeviceBase::WriteOnly)) {
+ const auto av =
+ static_cast<double>(QStorageInfo(m_SaveFile.fileName()).bytesAvailable());
+
+ log::error(
+ "failed to create temporary file for '{}', error {} ('{}'), {:.3f}GB available",
+ m_SaveFile.fileName(), m_SaveFile.error(), m_SaveFile.errorString(),
+ (av / 1024 / 1024 / 1024));
+
+ throw Exception(
+ QObject::tr(
+ "Failed to save '%1', could not create a temporary file: %2 (error %3)")
+ .arg(m_SaveFile.fileName())
+ .arg(m_SaveFile.errorString())
+ .arg(m_SaveFile.error()));
+ }
+}
+
+QSaveFile* SafeWriteFile::operator->()
+{
+ Q_ASSERT(m_SaveFile.isOpen());
+ return &m_SaveFile;
+}
+
+#else // Linux — use plain QFile (QSaveFile is unreliable across filesystems)
+
+SafeWriteFile::SafeWriteFile(const QString& fileName) : m_File(fileName)
+{
+ if (!m_File.open(QIODeviceBase::WriteOnly | QIODeviceBase::Truncate)) {
+ const auto av =
+ static_cast<double>(QStorageInfo(m_File.fileName()).bytesAvailable());
+
+ log::error("failed to open '{}' for writing, error {} ('{}'), {:.3f}GB available",
+ m_File.fileName(), m_File.error(), m_File.errorString(),
+ (av / 1024 / 1024 / 1024));
+
+ throw Exception(
+ QObject::tr("Failed to save '%1': %2 (error %3)")
+ .arg(m_File.fileName())
+ .arg(m_File.errorString())
+ .arg(m_File.error()));
+ }
+}
+
+DirectWriteFile* SafeWriteFile::operator->()
+{
+ Q_ASSERT(m_File.isOpen());
+ return &m_File;
+}
+
+#endif
+
+} // namespace MOBase
diff --git a/libs/uibase/src/scopeguard.cpp b/libs/uibase/src/scopeguard.cpp
new file mode 100644
index 0000000..a671d1e
--- /dev/null
+++ b/libs/uibase/src/scopeguard.cpp
@@ -0,0 +1,6 @@
+#include <uibase/scopeguard.h>
+
+namespace MOBase
+{
+
+} // namespace MOBase
diff --git a/libs/uibase/src/sortabletreewidget.cpp b/libs/uibase/src/sortabletreewidget.cpp
new file mode 100644
index 0000000..5cfbdd4
--- /dev/null
+++ b/libs/uibase/src/sortabletreewidget.cpp
@@ -0,0 +1,116 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2013 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
+*/
+
+#include <uibase/sortabletreewidget.h>
+#include <QDropEvent>
+
+namespace MOBase
+{
+
+SortableTreeWidget::SortableTreeWidget(QWidget* parent)
+ : QTreeWidget(parent), m_LocalMoveOnly(false)
+{}
+
+void SortableTreeWidget::setLocalMoveOnly(bool localOnly)
+{
+ m_LocalMoveOnly = localOnly;
+}
+
+void SortableTreeWidget::dropEvent(QDropEvent* event)
+{
+ // only react on internal drag&drop
+ if (event->source() == this) {
+ QTreeWidget::dropEvent(event);
+ }
+}
+
+Qt::DropActions SortableTreeWidget::supportedDropActions() const
+{
+ // we do our own drop handling, this ensures dropMimeData is called.
+ return Qt::CopyAction;
+}
+
+bool SortableTreeWidget::dropMimeData(QTreeWidgetItem* parent, int index,
+ const QMimeData*, Qt::DropAction)
+{
+ // TODO: ignores type of action set up in designer, always moves
+ return this->moveSelection(parent, index);
+}
+
+bool SortableTreeWidget::moveSelection(QTreeWidgetItem* parent, int idx)
+{
+ QModelIndex parentIndex = indexFromItem(parent);
+ std::vector<QPersistentModelIndex> persistentIndices;
+ Q_FOREACH (const QModelIndex& index, selectedIndexes()) {
+ if (index == parentIndex)
+ return false;
+ if (m_LocalMoveOnly && (parentIndex != index.parent()))
+ return false;
+ if (index.column() != 0)
+ continue;
+ persistentIndices.push_back(index);
+ }
+
+ int targetRow = -1;
+ if (itemsExpandable() || !parentIndex.isValid()) {
+ targetRow = model()->index(idx, 0, parentIndex).row();
+
+ if (targetRow < 0) {
+ targetRow = idx;
+ }
+ } else {
+ // if items aren't expandable, we can't be placing items on sublevels
+ targetRow = parentIndex.row();
+ parentIndex = QModelIndex();
+ }
+
+ // remove items from the list, store in temporary location
+ QList<QTreeWidgetItem*> temp;
+ for (auto iter = persistentIndices.rbegin(); iter != persistentIndices.rend();
+ ++iter) {
+ QTreeWidgetItem* item = itemFromIndex(*iter);
+ if (item == nullptr)
+ continue;
+ if ((item == nullptr) || (item->parent() == nullptr)) {
+ temp.append(takeTopLevelItem(iter->row()));
+ } else {
+ temp.append(item->parent()->takeChild(iter->row()));
+ }
+ }
+
+ if (idx == -1) {
+ // append
+ if (parentIndex.isValid()) {
+ parent->addChildren(temp);
+ } else {
+ addTopLevelItems(temp);
+ }
+ } else {
+ if (parentIndex.isValid()) {
+ parent->insertChildren(std::min<int>(targetRow, parent->childCount()), temp);
+ } else {
+ insertTopLevelItems(std::min<int>(targetRow, topLevelItemCount()), temp);
+ }
+ }
+ emit itemsMoved();
+ return true;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/steamutility.cpp b/libs/uibase/src/steamutility.cpp
new file mode 100644
index 0000000..a9dd80a
--- /dev/null
+++ b/libs/uibase/src/steamutility.cpp
@@ -0,0 +1,108 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2019 MO2 Contributors. 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
+*/
+
+#include <uibase/steamutility.h>
+
+#include <QDir>
+#include <QList>
+#include <QRegularExpression>
+#include <QSettings>
+#include <QString>
+#include <QTextStream>
+
+namespace MOBase
+{
+
+// Lines that contains libraries are in the format:
+// "1" "Path\to\library"
+static const QRegularExpression
+ kSteamLibraryFilter("^\\s*\"(?<idx>[0-9]+)\"\\s*\"(?<path>.*)\"");
+
+QString findSteam()
+{
+#ifdef _WIN32
+ QSettings steamRegistry("Valve", "Steam");
+ return steamRegistry.value("SteamPath").toString();
+#else
+ // On Linux, check common Steam locations
+ QString homePath = QDir::homePath();
+ QStringList candidates = {
+ homePath + "/.steam/steam",
+ homePath + "/.local/share/Steam",
+ homePath + "/.steam/debian-installation"
+ };
+ for (const auto& path : candidates) {
+ if (QDir(path).exists()) {
+ return path;
+ }
+ }
+ return "";
+#endif
+}
+
+QString findSteamGame(const QString& appName, const QString& validFile)
+{
+ QStringList libraryFolders; // list of Steam libraries to search
+ QDir steamDir(findSteam()); // Steam installation directory
+
+ // Can do nothing if Steam doesn't exist
+ if (!steamDir.exists())
+ return "";
+
+ // The Steam install is always a valid library
+ libraryFolders << steamDir.absolutePath();
+
+ // Search libraryfolders.vdf for additional libraries
+ QFile libraryFoldersFile(steamDir.absoluteFilePath("steamapps/libraryfolders.vdf"));
+ if (libraryFoldersFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ QTextStream in(&libraryFoldersFile);
+ while (!in.atEnd()) {
+ QString line = in.readLine();
+ QRegularExpressionMatch match = kSteamLibraryFilter.match(line);
+ if (match.hasMatch()) {
+ QString folder = match.captured("path");
+#ifdef _WIN32
+ folder.replace("/", "\\").replace("\\\\", "\\");
+#else
+ folder.replace("\\\\", "/").replace("\\", "/");
+#endif
+ libraryFolders << folder;
+ }
+ }
+ }
+
+ // Search the Steam libraries for the game directory
+ for (auto library : libraryFolders) {
+ QDir libraryDir(library);
+ if (!libraryDir.cd("steamapps/common/" + appName))
+ continue;
+ QString normalizedValidFile = validFile;
+ normalizedValidFile.replace("\\", "/");
+ if (normalizedValidFile.startsWith('/')) {
+ normalizedValidFile.remove(0, 1);
+ }
+ if (normalizedValidFile.isEmpty() || libraryDir.exists(normalizedValidFile))
+ return libraryDir.absolutePath();
+ }
+
+ return "";
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/strings.cpp b/libs/uibase/src/strings.cpp
new file mode 100644
index 0000000..11087f4
--- /dev/null
+++ b/libs/uibase/src/strings.cpp
@@ -0,0 +1,46 @@
+#include <uibase/strings.h>
+
+#include <algorithm>
+#include <locale>
+
+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 <typename T1, typename T2>
+ bool operator()(const T1& Arg1, const T2& Arg2) const
+ {
+ return std::toupper<T1>(Arg1, m_loc) == std::toupper<T2>(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<std::string::difference_type>(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/libs/uibase/src/taskdialog.ui b/libs/uibase/src/taskdialog.ui
new file mode 100644
index 0000000..727312d
--- /dev/null
+++ b/libs/uibase/src/taskdialog.ui
@@ -0,0 +1,359 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TaskDialog</class>
+ <widget class="QDialog" name="TaskDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>436</width>
+ <height>316</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Dialog</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="topPanel" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
+ <item>
+ <widget class="QWidget" name="iconPanel" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="leftMargin">
+ <number>9</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>9</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="icon">
+ <property name="text">
+ <string>icon</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="mainPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="main">
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="text">
+ <string>dummy main text</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="mainSpacer" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>10</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="contentPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="content">
+ <property name="text">
+ <string>dummy content text</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="buttonsSpacer" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>10</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7"/>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="commandButtonsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="commandButtons" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCommandLinkButton" name="dummyButton">
+ <property name="text">
+ <string>dummy button</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="rememberPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="dummyCheckbox">
+ <property name="text">
+ <string>dummy checkbox</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QWidget" name="standardButtonsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QDialogButtonBox" name="standardButtons">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="detailsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_7" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QToolButton" name="detailsExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>Details</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="detailsWidget" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>9</number>
+ </property>
+ <item>
+ <widget class="QPlainTextEdit" name="details">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/uibase/src/taskprogressmanager.cpp b/libs/uibase/src/taskprogressmanager.cpp
new file mode 100644
index 0000000..3bcffb0
--- /dev/null
+++ b/libs/uibase/src/taskprogressmanager.cpp
@@ -0,0 +1,125 @@
+#include <uibase/taskprogressmanager.h>
+#include <uibase/log.h>
+#include <uibase/utility.h>
+#include <QApplication>
+#include <QMainWindow>
+#include <QWidget>
+
+namespace MOBase
+{
+
+TaskProgressManager& TaskProgressManager::instance()
+{
+ static TaskProgressManager s_Instance;
+ return s_Instance;
+}
+
+void TaskProgressManager::forgetMe(quint32 id)
+{
+ if (m_Taskbar == nullptr) {
+ return;
+ }
+ auto iter = m_Percentages.find(id);
+ if (iter != m_Percentages.end()) {
+ m_Percentages.erase(iter);
+ }
+ showProgress();
+}
+
+void TaskProgressManager::updateProgress(quint32 id, qint64 value, qint64 max)
+{
+ QMutexLocker lock(&m_Mutex);
+ if (m_Taskbar == nullptr) {
+ return;
+ }
+
+ if (value == max) {
+ auto iter = m_Percentages.find(id);
+ if (iter != m_Percentages.end()) {
+ m_Percentages.erase(iter);
+ }
+ } else {
+ m_Percentages[id] = std::make_pair(QTime::currentTime(), (value * 100) / max);
+ }
+
+ showProgress();
+}
+
+quint32 TaskProgressManager::getId()
+{
+ QMutexLocker lock(&m_Mutex);
+ return m_NextId++;
+}
+
+void TaskProgressManager::showProgress()
+{
+#ifdef _WIN32
+ if (!m_Percentages.empty()) {
+ m_Taskbar->SetProgressState(m_WinId, TBPF_NORMAL);
+
+ QTime now = QTime::currentTime();
+ unsigned long long total = 0;
+ unsigned long long count = 0;
+
+ for (auto iter = m_Percentages.begin(); iter != m_Percentages.end();) {
+ if (iter->second.first.secsTo(now) < 15) {
+ total += static_cast<unsigned long long>(iter->second.second);
+ ++iter;
+ ++count;
+ } else {
+ log::debug("no progress in 15 seconds ({})", iter->second.first.secsTo(now));
+ iter = m_Percentages.erase(iter);
+ }
+ }
+
+ m_Taskbar->SetProgressValue(m_WinId, total, count * 100);
+ } else {
+ m_Taskbar->SetProgressState(m_WinId, TBPF_NOPROGRESS);
+ }
+#else
+ // On Linux, taskbar progress is not supported in this implementation.
+ // Could integrate with D-Bus com.canonical.Unity.LauncherEntry or similar.
+ (void)m_Percentages;
+#endif
+}
+
+bool TaskProgressManager::tryCreateTaskbar()
+{
+#ifdef _WIN32
+ // try to find our main window
+ for (QWidget* widget : QApplication::topLevelWidgets()) {
+ QMainWindow* mainWin = qobject_cast<QMainWindow*>(widget);
+ if (mainWin != nullptr) {
+ m_WinId = reinterpret_cast<HWND>(mainWin->winId());
+ }
+ }
+
+ HRESULT result = 0;
+ if (m_WinId != nullptr) {
+ result = CoCreateInstance(CLSID_TaskbarList, 0, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(&m_Taskbar));
+ if (result == S_OK) {
+ return true;
+ }
+ }
+
+ m_Taskbar = nullptr;
+
+ if (m_CreateTries-- > 0) {
+ QTimer::singleShot(1000, this, SLOT(tryCreateTaskbar()));
+ } else {
+ log::warn("failed to create taskbar connection");
+ }
+ return false;
+#else
+ // On Linux, no Windows taskbar API
+ m_Taskbar = nullptr;
+ return false;
+#endif
+}
+
+TaskProgressManager::TaskProgressManager()
+ : m_NextId(1), m_CreateTries(10), m_WinId(nullptr), m_Taskbar(nullptr)
+{}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/textviewer.cpp b/libs/uibase/src/textviewer.cpp
new file mode 100644
index 0000000..8c7ac0f
--- /dev/null
+++ b/libs/uibase/src/textviewer.cpp
@@ -0,0 +1,278 @@
+/*
+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
+*/
+
+#include <uibase/textviewer.h>
+#include <uibase/finddialog.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include "ui_textviewer.h"
+#include <uibase/utility.h>
+#include <QAction>
+#include <QFile>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QShortcutEvent>
+#include <QTextEdit>
+#include <QVBoxLayout>
+
+namespace MOBase
+{
+
+TextViewer::TextViewer(const QString& title, QWidget* parent)
+ : QDialog(parent), ui(new Ui::TextViewer), m_FindDialog(nullptr)
+{
+ ui->setupUi(this);
+ setWindowTitle(title);
+ m_EditorTabs = findChild<QTabWidget*>("editorTabs");
+ connect(ui->showWhitespace, SIGNAL(stateChanged(int)), this,
+ SLOT(showWhitespaceChanged(int)));
+}
+
+TextViewer::~TextViewer()
+{
+ delete ui;
+}
+
+void TextViewer::closeEvent(QCloseEvent* event)
+{
+ if (!m_Modified.empty()) {
+ for (std::set<QTextEdit*>::iterator iter = m_Modified.begin();
+ iter != m_Modified.end(); ++iter) {
+ QMessageBox::StandardButton res = QMessageBox::question(
+ this, tr("Save changes?"),
+ tr("Do you want to save changes to %1?").arg((*iter)->documentTitle()),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
+ if (res == QMessageBox::Yes) {
+ saveFile(*iter);
+ } else if (res == QMessageBox::Cancel) {
+ event->ignore();
+ break;
+ }
+ }
+ }
+}
+
+void TextViewer::find()
+{
+ if (!m_FindDialog) {
+ m_FindDialog = new FindDialog(this);
+ connect(m_FindDialog, SIGNAL(findNext()), this, SLOT(findNext()));
+ connect(m_FindDialog, SIGNAL(patternChanged(QString)), this,
+ SLOT(patternChanged(QString)));
+ }
+
+ m_FindDialog->show();
+ m_FindDialog->raise();
+ m_FindDialog->activateWindow();
+}
+
+void TextViewer::patternChanged(QString newPattern)
+{
+ m_FindPattern = newPattern;
+}
+
+void TextViewer::findNext()
+{
+ if (m_FindPattern.length() == 0) {
+ return;
+ }
+
+ QWidget* currentPage = m_EditorTabs->currentWidget();
+ QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView");
+
+ if (editor->find(m_FindPattern)) {
+ // found text
+ return;
+ } else {
+ // reached the bottom and no text found,
+ // we wrap around once.
+
+ // save current cursor
+ auto oldCursor = editor->textCursor();
+
+ editor->moveCursor(QTextCursor::Start);
+
+ // search again from the top
+ if (editor->find(m_FindPattern)) {
+ // found something, keep new cursor position.
+ return;
+ } else {
+ // there are no matches in the document,
+ // restore previous cursor.
+ editor->setTextCursor(oldCursor);
+ }
+ }
+}
+
+void TextViewer::showWhitespaceChanged(int state)
+{
+ for (int i = 0; i < m_EditorTabs->count(); ++i) {
+ QTextEdit* editor = m_EditorTabs->widget(i)->findChild<QTextEdit*>();
+ if (editor != nullptr) {
+ auto document = editor->document();
+ auto textOption = document->defaultTextOption();
+ auto flags = textOption.flags();
+ if (state == Qt::Unchecked)
+ flags = flags & (~QTextOption::ShowTabsAndSpaces);
+ else
+ flags = flags | QTextOption::ShowTabsAndSpaces;
+ textOption.setFlags(flags);
+ document->setDefaultTextOption(textOption);
+ editor->setDocument(document);
+ }
+ }
+}
+
+bool TextViewer::eventFilter(QObject* object, QEvent* event)
+{
+ if (event->type() == QEvent::ShortcutOverride) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->matches(QKeySequence::Find)) {
+ find();
+ } else if (keyEvent->matches(QKeySequence::FindNext)) {
+ findNext();
+ }
+ }
+ return QDialog::eventFilter(object, event);
+}
+
+void TextViewer::setDescription(const QString& description)
+{
+ QLabel* descriptionLabel = findChild<QLabel*>("descriptionLabel");
+ descriptionLabel->setText(description);
+}
+
+void TextViewer::saveFile(const QTextEdit* editor)
+{
+ bool write = true;
+ QMessageBox::StandardButton buttonPressed = QMessageBox::Ignore;
+ QFile file(editor->documentTitle());
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ write = false;
+ QFileInfo fileInfo(file.fileName());
+ buttonPressed =
+ MOBase::TaskDialog(qApp->activeModalWidget(),
+ QObject::tr("INI file is read-only"))
+ .main(QObject::tr("INI file is read-only"))
+ .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" which "
+ "is currently set to read-only.")
+ .arg(fileInfo.fileName()))
+ .icon(QMessageBox::Warning)
+ .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes})
+ .button({QObject::tr("Allow the write once"),
+ QObject::tr("The file will be set to read-only again."),
+ QMessageBox::Ignore})
+ .button({QObject::tr("Skip this file"), QMessageBox::No})
+ .remember("clearReadOnly", fileInfo.fileName())
+ .exec();
+
+ if (buttonPressed & (QMessageBox::Yes | QMessageBox::Ignore)) {
+ file.setPermissions(file.permissions() | QFile::WriteUser);
+ }
+
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ reportError(tr("failed to write to %1").arg(editor->documentTitle()));
+ } else {
+ write = true;
+ }
+ }
+
+ if (write) {
+ file.write(editor->toPlainText().toUtf8().replace('\n', "\r\n"));
+ file.close();
+ }
+
+ if (buttonPressed == QMessageBox::Ignore) {
+ file.setPermissions(file.permissions() & ~(QFile::WriteUser));
+ }
+}
+
+void TextViewer::saveFile()
+{
+ QWidget* currentPage = m_EditorTabs->currentWidget();
+ QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView");
+ saveFile(editor);
+
+ m_Modified.erase(editor);
+}
+
+void TextViewer::modified()
+{
+ QWidget* currentPage = m_EditorTabs->currentWidget();
+ QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView");
+
+ m_Modified.insert(editor);
+}
+
+void TextViewer::addFile(const QString& fileName, bool writable)
+{
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly)) {
+ throw Exception(tr("file not found: %1").arg(fileName));
+ }
+ QByteArray temp = file.readAll();
+
+ QWidget* page = new QWidget();
+ QVBoxLayout* layout = new QVBoxLayout(page);
+ QTextEdit* editor = new QTextEdit(page);
+ QTextDocument* document = new QTextDocument(page);
+ if (ui->showWhitespace->isChecked()) {
+ QTextOption option;
+ option.setFlags(QTextOption::ShowTabsAndSpaces);
+ document->setDefaultTextOption(option);
+ }
+ editor->setDocument(document);
+ editor->setAcceptRichText(false);
+ editor->setPlainText(QString(temp));
+ editor->setLineWrapMode(QTextEdit::NoWrap);
+ editor->setObjectName("editorView");
+ editor->setDocumentTitle(fileName);
+ editor->installEventFilter(this);
+ editor->setReadOnly(!writable);
+
+ // set text highlighting color in inactive window equal to text hightlighting color in
+ // active window
+ QPalette palette = editor->palette();
+ palette.setColor(QPalette::Inactive, QPalette::Highlight,
+ palette.color(QPalette::Active, QPalette::Highlight));
+ palette.setColor(QPalette::Inactive, QPalette::HighlightedText,
+ palette.color(QPalette::Active, QPalette::HighlightedText));
+ editor->setPalette(palette);
+
+ // add hotkeys for searching through the document
+ QAction* findAction = new QAction(QString("&Find"), editor);
+ findAction->setShortcut(QKeySequence::Find);
+ editor->addAction(findAction);
+ QAction* findNextAction = new QAction(QString("Find &Next"), editor);
+ findAction->setShortcut(QKeySequence::FindNext);
+ editor->addAction(findNextAction);
+
+ layout->addWidget(editor);
+ if (writable) {
+ QPushButton* saveBtn = new QPushButton(tr("Save"), page);
+ layout->addWidget(saveBtn);
+ connect(saveBtn, SIGNAL(clicked()), this, SLOT(saveFile()));
+ connect(editor, SIGNAL(textChanged()), this, SLOT(modified()));
+ }
+ page->setLayout(layout);
+ m_EditorTabs->addTab(page, QFileInfo(fileName).fileName());
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/textviewer.ui b/libs/uibase/src/textviewer.ui
new file mode 100644
index 0000000..e2af30e
--- /dev/null
+++ b/libs/uibase/src/textviewer.ui
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TextViewer</class>
+ <widget class="QDialog" name="TextViewer">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>607</width>
+ <height>528</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Log Viewer</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="text">
+ <string>Placeholder</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showWhitespace">
+ <property name="text">
+ <string>Show Whitespace</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="editorTabs">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/uibase/src/tutorabledialog.cpp b/libs/uibase/src/tutorabledialog.cpp
new file mode 100644
index 0000000..61aad62
--- /dev/null
+++ b/libs/uibase/src/tutorabledialog.cpp
@@ -0,0 +1,40 @@
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/tutorabledialog.h>
+
+namespace MOBase
+{
+
+TutorableDialog::TutorableDialog(const QString& name, QWidget* parent)
+ : QDialog(parent), m_Tutorial(this, name)
+{}
+
+void TutorableDialog::showEvent(QShowEvent* event)
+{
+ m_Tutorial.registerControl();
+ QDialog::showEvent(event);
+}
+
+void TutorableDialog::resizeEvent(QResizeEvent* event)
+{
+ m_Tutorial.resize(event->size());
+ QDialog::resizeEvent(event);
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/tutorialcontrol.cpp b/libs/uibase/src/tutorialcontrol.cpp
new file mode 100644
index 0000000..fee19f4
--- /dev/null
+++ b/libs/uibase/src/tutorialcontrol.cpp
@@ -0,0 +1,384 @@
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/tutorialcontrol.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <uibase/tutorialmanager.h>
+#include <uibase/utility.h>
+
+#include <QAbstractButton>
+#include <QAction>
+#include <QBitmap>
+#include <QCoreApplication>
+#include <QDir>
+#include <QFile>
+#include <QGraphicsObject>
+#include <QImage>
+#include <QMenuBar>
+#include <QMouseEvent>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QQuickItem>
+#include <QTableWidget>
+#include <QTimer>
+#include <QToolBar>
+
+#include <QMetaEnum>
+#include <QMetaMethod>
+#include <QMetaObject>
+#include <qmetaobject.h>
+
+namespace MOBase
+{
+
+TutorialControl::TutorialControl(const TutorialControl& reference)
+ : QObject(reference.parent()), m_TargetControl(reference.m_TargetControl),
+ m_Name(reference.m_Name), m_TutorialView(nullptr),
+ m_Manager(TutorialManager::instance()), m_ExpectedTab(0),
+ m_CurrentClickControl(nullptr)
+{}
+
+TutorialControl::TutorialControl(QWidget* targetControl, const QString& name)
+ : QObject(nullptr), m_TargetControl(targetControl), m_Name(name),
+ m_TutorialView(nullptr), m_Manager(TutorialManager::instance()), m_ExpectedTab(0),
+ m_CurrentClickControl(nullptr)
+{}
+
+TutorialControl::~TutorialControl()
+{
+ m_Manager.unregisterControl(m_Name);
+ finish();
+}
+
+void TutorialControl::registerControl()
+{
+ m_Manager.registerControl(m_Name, this);
+}
+
+void TutorialControl::resize(const QSize& size)
+{
+ if (m_TutorialView != nullptr) {
+ m_TutorialView->resize(size.width(), size.height());
+ }
+}
+
+void TutorialControl::expose(const QString& widgetName, QObject* widget)
+{
+ m_ExposedObjects.push_back(std::make_pair(widgetName, widget));
+}
+
+static QString canonicalPath(const QString& path)
+{
+#ifdef _WIN32
+ std::unique_ptr<wchar_t[]> buffer(new wchar_t[32768]);
+ DWORD res = ::GetShortPathNameW((wchar_t*)path.utf16(), buffer.get(), 32768);
+ if (res == 0) {
+ return path;
+ }
+ res = ::GetLongPathNameW(buffer.get(), buffer.get(), 32768);
+ if (res == 0) {
+ return path;
+ }
+ return QString::fromWCharArray(buffer.get());
+#else
+ QFileInfo fi(path);
+ QString canonical = fi.canonicalFilePath();
+ return canonical.isEmpty() ? path : canonical;
+#endif
+}
+
+void TutorialControl::startTutorial(const QString& tutorial)
+{
+ if (m_TutorialView == nullptr) {
+ m_TutorialView = new QQuickWidget(m_TargetControl);
+ m_TutorialView->setResizeMode(QQuickWidget::SizeRootObjectToView);
+ m_TutorialView->setAttribute(Qt::WA_TranslucentBackground);
+ m_TutorialView->setAttribute(Qt::WA_AlwaysStackOnTop);
+ m_TutorialView->setClearColor(Qt::transparent);
+ m_TutorialView->setStyleSheet("background: transparent");
+ m_TutorialView->setObjectName("tutorialView");
+ m_TutorialView->rootContext()->setContextProperty("manager", &m_Manager);
+
+ QString qmlName =
+ canonicalPath(QCoreApplication::applicationDirPath() + "/tutorials") +
+ "/tutorials_" + m_Name.toLower() + ".qml";
+ QUrl qmlSource = QUrl::fromLocalFile(qmlName);
+
+ m_TutorialView->setSource(qmlSource);
+ m_TutorialView->resize(m_TargetControl->width(), m_TargetControl->height());
+ m_TutorialView->rootContext()->setContextProperty("scriptName", tutorial);
+ m_TutorialView->rootContext()->setContextProperty("tutorialControl", this);
+ m_TutorialView->rootContext()->setContextProperty("applicationWindow",
+ m_TargetControl);
+ m_TutorialView->rootContext()->setContextProperty("organizer",
+ m_Manager.organizerCore());
+
+ for (std::vector<std::pair<QString, QObject*>>::const_iterator iter =
+ m_ExposedObjects.begin();
+ iter != m_ExposedObjects.end(); ++iter) {
+ m_TutorialView->rootContext()->setContextProperty(iter->first, iter->second);
+ }
+ m_TutorialView->show();
+ m_TutorialView->raise();
+ if (!QMetaObject::invokeMethod(m_TutorialView->rootObject(), "init")) {
+ reportError(tr(
+ "Tutorial failed to start, please check \"mo_interface.log\" for details."));
+ m_TutorialView->close();
+ }
+ }
+}
+
+void TutorialControl::lockUI(bool locked)
+{
+ m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, !locked);
+
+ QMetaObject::invokeMethod(m_TutorialView->rootObject(), "enableBackground",
+ Q_ARG(QVariant, QVariant(locked)));
+}
+
+void TutorialControl::simulateClick(int x, int y)
+{
+ bool wasTransparent = m_TutorialView->testAttribute(Qt::WA_TransparentForMouseEvents);
+ 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, QPointF(hitPos), QPointF(globalPos),
+ Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+ QMouseEvent* upEvent =
+ new QMouseEvent(QEvent::MouseButtonRelease, QPointF(hitPos), QPointF(globalPos),
+ Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+
+ qApp->postEvent(hitControl, (QEvent*)downEvent);
+ qApp->postEvent(hitControl, (QEvent*)upEvent);
+
+ if (!wasTransparent) {
+ m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, false);
+ }
+}
+
+QWidget* TutorialControl::getChild(const QString& name)
+{
+ if (m_TargetControl != nullptr) {
+ return m_TargetControl->findChild<QWidget*>(name);
+ } else {
+ return nullptr;
+ }
+}
+
+void TutorialControl::finish()
+{
+ if (m_TutorialView != nullptr) {
+ m_TutorialView->deleteLater();
+ }
+ m_TutorialView = nullptr;
+ m_TutorialView = nullptr;
+}
+
+QRect TutorialControl::getRect(const QString& widgetName)
+{
+ if (m_TargetControl != nullptr) {
+ QWidget* widget = m_TargetControl->findChild<QWidget*>(widgetName);
+ if (widget != nullptr) {
+ QRect res = widget->rect();
+ QPoint pos = widget->mapTo(m_TargetControl, res.topLeft());
+ res.moveTopLeft(pos);
+ return res;
+ } else {
+ log::error("{} not found", widgetName);
+ return QRect();
+ }
+ } else {
+ return QRect();
+ }
+}
+
+QRect TutorialControl::getActionRect(const QString& widgetName)
+{
+ if (m_TargetControl != nullptr) {
+ QToolBar* toolBar = m_TargetControl->findChild<QToolBar*>("toolBar");
+ foreach (QAction* action, toolBar->actions()) {
+ if (action->objectName() == widgetName) {
+ return toolBar->actionGeometry(action);
+ }
+ }
+ }
+ return QRect();
+}
+
+QRect TutorialControl::getMenuRect(const QString&)
+{
+ if (m_TargetControl != nullptr) {
+ QMenuBar* menuBar = m_TargetControl->findChild<QMenuBar*>("menuBar");
+ return menuBar->geometry();
+ }
+ return QRect();
+}
+
+void TutorialControl::nextTutorialStepProxy()
+{
+
+ if (m_TutorialView != nullptr) {
+ QObject* background = m_TutorialView->rootObject();
+
+ QTimer::singleShot(1, background, SLOT(nextStep()));
+ lockUI(true);
+
+ bool success = true;
+ for (QMetaObject::Connection connection : m_Connections) {
+ if (!disconnect(connection)) {
+ success = false;
+ }
+ }
+ m_Connections.clear();
+ if (!success) {
+ log::error("failed to disconnect tutorial proxy");
+ }
+ } else {
+ log::error("failed to proceed to next tutorial step");
+ finish();
+ }
+}
+
+void TutorialControl::tabChangedProxy(int selected)
+{
+ if ((m_TutorialView != nullptr) && (selected == m_ExpectedTab)) {
+ QObject* background = m_TutorialView->rootObject();
+ QTimer::singleShot(1, background, SLOT(nextStep()));
+ lockUI(true);
+ bool success = true;
+ for (QMetaObject::Connection connection : m_Connections) {
+ if (!disconnect(connection)) {
+ success = false;
+ }
+ }
+ m_Connections.clear();
+ if (!success) {
+ log::error("failed to disconnect tab-changed proxy");
+ }
+ }
+}
+
+bool TutorialControl::waitForAction(const QString& actionName)
+{
+ if (m_TargetControl != nullptr) {
+ QAction* action = m_TargetControl->findChild<QAction*>(actionName);
+ if (action == nullptr) {
+ log::error("no action \"{}\" in control \"{}\"", actionName, m_Name);
+ return false;
+ }
+ if (action->isEnabled()) {
+ if (action->menu() != nullptr) {
+ m_Connections.append(connect(action->menu(), SIGNAL(aboutToShow()), this,
+ SLOT(nextTutorialStepProxy())));
+ } else {
+ m_Connections.append(
+ connect(action, SIGNAL(triggered()), this, SLOT(nextTutorialStepProxy())));
+ }
+ lockUI(false);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+bool TutorialControl::waitForButton(const QString& buttonName)
+{
+ if (m_TargetControl != nullptr) {
+ QAbstractButton* button = m_TargetControl->findChild<QAbstractButton*>(buttonName);
+ if (button == nullptr) {
+ log::error("no button \"{}\" in control \"{}\"", buttonName, m_Name);
+ return false;
+ }
+ if (button->isEnabled()) {
+ m_Connections.append(
+ connect(button, SIGNAL(pressed()), this, SLOT(nextTutorialStepProxy())));
+ lockUI(false);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+bool TutorialControl::waitForTabOpen(const QString& tabControlName, const QString& tab)
+{
+ if (m_TargetControl != nullptr) {
+ QTabWidget* tabWidget = m_TargetControl->findChild<QTabWidget*>(tabControlName);
+ if (tabWidget == nullptr) {
+ log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name);
+ return false;
+ }
+ if (tabWidget->findChild<QWidget*>(tab) == nullptr) {
+ log::error("no widget \"{}\" found in tab widget \"{}\"", tab, tabControlName);
+ return false;
+ }
+ int tabIndex = tabWidget->indexOf(tabWidget->findChild<QWidget*>(tab));
+ if (tabIndex == -1) {
+ log::error("widget \"{}\" does not appear to be a tab in tab widget \"{}\"", tab,
+ tabControlName);
+ return false;
+ }
+ if (tabWidget->isEnabled()) {
+ if (tabWidget->currentIndex() != tabIndex) {
+ m_ExpectedTab = tabIndex;
+ m_Connections.append(connect(tabWidget, SIGNAL(currentChanged(int)), this,
+ SLOT(tabChangedProxy(int))));
+ lockUI(false);
+ } else {
+ QObject* background = m_TutorialView->rootObject();
+ QTimer::singleShot(1, background, SLOT(nextStep()));
+ lockUI(true);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+const QString TutorialControl::getTabName(const QString& tabControlName)
+{
+ if (m_TargetControl != nullptr) {
+ QTabWidget* tabWidget = m_TargetControl->findChild<QTabWidget*>(tabControlName);
+ if (tabWidget == nullptr) {
+ log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name);
+ return QString();
+ }
+ if (tabWidget->currentIndex() == -1) {
+ return QString();
+ } else {
+ return tabWidget->currentWidget()->objectName();
+ }
+ } else {
+ return QString();
+ }
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/tutorialmanager.cpp b/libs/uibase/src/tutorialmanager.cpp
new file mode 100644
index 0000000..58191e7
--- /dev/null
+++ b/libs/uibase/src/tutorialmanager.cpp
@@ -0,0 +1,112 @@
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/tutorialmanager.h>
+#include <uibase/log.h>
+#include <uibase/tutorialcontrol.h>
+#include <uibase/utility.h>
+#include <QApplication>
+#include <QDir>
+#include <QList>
+#include <QString>
+#include <QTimer>
+
+namespace MOBase
+{
+
+TutorialManager* TutorialManager::s_Instance = nullptr;
+
+TutorialManager::TutorialManager(const QString& tutorialPath, QObject* organizerCore)
+ : m_TutorialPath(tutorialPath), m_OrganizerCore(organizerCore)
+{}
+
+void TutorialManager::init(const QString& tutorialPath, QObject* organizerCore)
+{
+ if (s_Instance != nullptr) {
+ delete s_Instance;
+ }
+ s_Instance = new TutorialManager(tutorialPath, organizerCore);
+}
+
+TutorialManager& TutorialManager::instance()
+{
+ if (s_Instance == nullptr) {
+ throw Exception(tr("tutorial manager not set up yet"));
+ }
+ return *s_Instance;
+}
+
+void TutorialManager::activateTutorial(const QString& windowName,
+ const QString& tutorialName)
+{
+ std::map<QString, TutorialControl*>::iterator iter = m_Controls.find(windowName);
+ if (iter != m_Controls.end()) {
+ // control already visible, start tutorial right away
+ iter->second->startTutorial(m_TutorialPath + tutorialName);
+ } else {
+ m_PendingTutorials[windowName] = tutorialName;
+ }
+}
+
+void TutorialManager::finishWindowTutorial(const QString& windowName)
+{
+ emit windowTutorialFinished(windowName);
+ // QSettings &settings = Settings::instance().directInterface();
+ // settings.setValue(QString("CompletedWindowTutorials/") + windowName, true);
+}
+
+bool TutorialManager::hasTutorial(const QString& tutorialName)
+{
+ return QFile::exists(m_TutorialPath + tutorialName);
+}
+
+QWidget* TutorialManager::findControl(const QString& controlName)
+{
+ QWidget* mainWindow = qApp->activeWindow();
+ if (mainWindow != nullptr) {
+ return mainWindow->findChild<QWidget*>(controlName);
+ } else {
+ return nullptr;
+ }
+}
+
+void TutorialManager::registerControl(const QString& windowName,
+ TutorialControl* control)
+{
+ m_Controls[windowName] = control;
+ std::map<QString, QString>::iterator iter = m_PendingTutorials.find(windowName);
+ if (iter != m_PendingTutorials.end()) {
+ // there is a pending tutorial for this window, display it
+ QTimer::singleShot(0, [control, tutorial = m_TutorialPath + iter->second] {
+ control->startTutorial(tutorial);
+ });
+ m_PendingTutorials.erase(iter);
+ }
+}
+
+void TutorialManager::unregisterControl(const QString& windowName)
+{
+ std::map<QString, TutorialControl*>::iterator iter = m_Controls.find(windowName);
+ if (iter != m_Controls.end()) {
+ m_Controls.erase(iter);
+ } else {
+ log::warn("failed to remove tutorial control {}", windowName);
+ }
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/uibase_en.ts b/libs/uibase/src/uibase_en.ts
new file mode 100644
index 0000000..defc545
--- /dev/null
+++ b/libs/uibase/src/uibase_en.ts
@@ -0,0 +1,407 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>FilterWidget</name>
+ <message>
+ <location filename="filterwidget.cpp" line="610"/>
+ <source>Filter options</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="617"/>
+ <source>Use regular expressions</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="618"/>
+ <source>Use regular expressions in filters</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="627"/>
+ <source>Case sensitive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="629"/>
+ <source>Make regular expressions case sensitive (/i)</source>
+ <extracomment>leave &quot;(/i)&quot; verbatim</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="639"/>
+ <source>Extended</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="641"/>
+ <source>Ignores unescaped whitespace in regular expressions (/x)</source>
+ <extracomment>leave &quot;(/x)&quot; verbatim</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="651"/>
+ <source>Keep selection in view</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="652"/>
+ <source>Scroll to keep the current selection in view after filtering</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FindDialog</name>
+ <message>
+ <location filename="finddialog.ui" line="14"/>
+ <source>Find</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="24"/>
+ <source>Find what:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="31"/>
+ <location filename="finddialog.ui" line="34"/>
+ <source>Search term</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="47"/>
+ <location filename="finddialog.ui" line="50"/>
+ <source>Find next occurence from current file position.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="53"/>
+ <source>&amp;Find Next</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="60"/>
+ <location filename="finddialog.ui" line="63"/>
+ <location filename="finddialog.ui" line="66"/>
+ <source>Close</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="filterwidget.cpp" line="128"/>
+ <source>Filter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginrequirements.cpp" line="31"/>
+ <source>One of the following plugins must be enabled: %1.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginrequirements.cpp" line="34"/>
+ <source>This plugin can only be enabled if the &apos;%1&apos; plugin is installed and enabled.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="pluginrequirements.cpp" line="63"/>
+ <source>This plugin can only be enabled for the following game(s): %1.</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="43"/>
+ <location filename="registry.cpp" line="44"/>
+ <location filename="textviewer.cpp" line="173"/>
+ <location filename="textviewer.cpp" line="174"/>
+ <source>INI file is read-only</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="45"/>
+ <location filename="textviewer.cpp" line="175"/>
+ <source>Mod Organizer is attempting to write to &quot;%1&quot; which is currently set to read-only.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="49"/>
+ <location filename="textviewer.cpp" line="179"/>
+ <source>Clear the read-only flag</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="50"/>
+ <location filename="textviewer.cpp" line="180"/>
+ <source>Allow the write once</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="51"/>
+ <location filename="textviewer.cpp" line="181"/>
+ <source>The file will be set to read-only again.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="53"/>
+ <location filename="textviewer.cpp" line="183"/>
+ <source>Skip this file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="65"/>
+ <location filename="report.cpp" line="71"/>
+ <source>Error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="338"/>
+ <source>You can reset these choices by clicking &quot;Reset Dialog Choices&quot; in the General tab of the Settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="347"/>
+ <source>Always ask</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="348"/>
+ <location filename="report.cpp" line="356"/>
+ <source>Remember my choice</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="350"/>
+ <source>Remember my choice for %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="safewritefile.cpp" line="40"/>
+ <source>Failed to save &apos;%1&apos;, could not create a temporary file: %2 (error %3)</source>
+ <oldsource>Failed to save &apos;{}&apos;, could not create a temporary file: {} (error {})</oldsource>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="62"/>
+ <source>removal of &quot;%1&quot; failed: %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="71"/>
+ <source>removal of &quot;%1&quot; failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="75"/>
+ <source>&quot;%1&quot; doesn&apos;t exist (remove)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="350"/>
+ <source>Error %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="534"/>
+ <location filename="utility.cpp" line="557"/>
+ <source>You have an invalid custom browser command in the settings.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="673"/>
+ <location filename="utility.cpp" line="702"/>
+ <source>failed to create directory &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="682"/>
+ <location filename="utility.cpp" line="709"/>
+ <source>failed to copy &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1134"/>
+ <source>%1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1134"/>
+ <source>%1 KB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1135"/>
+ <source>%1 MB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1135"/>
+ <source>%1 GB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1136"/>
+ <source>%1 TB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1141"/>
+ <source>%1 B/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1141"/>
+ <source>%1 KB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1142"/>
+ <source>%1 MB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1142"/>
+ <source>%1 GB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1143"/>
+ <source>%1 TB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QuestionBoxMemory</name>
+ <message>
+ <location filename="questionboxmemory.ui" line="106"/>
+ <source>Remember selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="questionboxmemory.ui" line="113"/>
+ <source>Remember selection only for %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TaskDialog</name>
+ <message>
+ <location filename="taskdialog.ui" line="20"/>
+ <source>Dialog</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="56"/>
+ <source>icon</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="102"/>
+ <source>dummy main text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="146"/>
+ <source>dummy content text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="206"/>
+ <source>dummy button</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="234"/>
+ <source>dummy checkbox</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="320"/>
+ <source>Details</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TextViewer</name>
+ <message>
+ <location filename="textviewer.ui" line="14"/>
+ <source>Log Viewer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.ui" line="22"/>
+ <source>Placeholder</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.ui" line="29"/>
+ <source>Show Whitespace</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="60"/>
+ <source>Save changes?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="61"/>
+ <source>Do you want to save changes to %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="192"/>
+ <source>failed to write to %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="229"/>
+ <source>file not found: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="270"/>
+ <source>Save</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TutorialControl</name>
+ <message>
+ <location filename="tutorialcontrol.cpp" line="135"/>
+ <source>Tutorial failed to start, please check &quot;mo_interface.log&quot; for details.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TutorialManager</name>
+ <message>
+ <location filename="tutorialmanager.cpp" line="50"/>
+ <source>tutorial manager not set up yet</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>uibase</name>
+ <message>
+ <location filename="utility.cpp" line="1205"/>
+ <source>h</source>
+ <extracomment>Time remaining hours</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1208"/>
+ <source>m</source>
+ <extracomment>Time remaining minutes</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1211"/>
+ <source>s</source>
+ <extracomment>Time remaining seconds</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp
new file mode 100644
index 0000000..0ad64ad
--- /dev/null
+++ b/libs/uibase/src/utility.cpp
@@ -0,0 +1,1260 @@
+/*
+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
+*/
+
+#include <uibase/utility.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <QApplication>
+#include <QBuffer>
+#include <QCollator>
+#include <QDesktopServices>
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QImage>
+#include <QImageReader>
+#include <QLayout>
+#include <QProcess>
+#include <QRegularExpression>
+#include <QScreen>
+#include <QStandardPaths>
+#include <QStringEncoder>
+#include <QUrl>
+#include <QUuid>
+#include <QtDebug>
+#include <cerrno>
+#include <cstring>
+#include <format>
+#include <iostream>
+#include <memory>
+#include <sstream>
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+#define FO_RECYCLE 0x1003
+#endif
+
+namespace MOBase
+{
+
+bool removeDir(const QString& dirName)
+{
+ QDir dir(dirName);
+
+ if (dir.exists()) {
+ Q_FOREACH (QFileInfo info,
+ dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
+ QDir::AllDirs | QDir::Files,
+ QDir::DirsFirst)) {
+ if (info.isDir()) {
+ if (!removeDir(info.absoluteFilePath())) {
+ return false;
+ }
+ } else {
+ // On Linux, just make sure file is writable before removing
+ QFile file(info.absoluteFilePath());
+ file.setPermissions(file.permissions() | QFile::WriteUser);
+ if (!file.remove()) {
+ reportError(QObject::tr("removal of \"%1\" failed: %2")
+ .arg(info.absoluteFilePath())
+ .arg(file.errorString()));
+ return false;
+ }
+ }
+ }
+
+ if (!dir.rmdir(dirName)) {
+ reportError(QObject::tr("removal of \"%1\" failed").arg(dir.absolutePath()));
+ return false;
+ }
+ } else {
+ reportError(QObject::tr("\"%1\" doesn't exist (remove)").arg(dirName));
+ return false;
+ }
+
+ return true;
+}
+
+bool copyDir(const QString& sourceName, const QString& destinationName, bool merge)
+{
+ QDir sourceDir(sourceName);
+ if (!sourceDir.exists()) {
+ return false;
+ }
+ QDir destDir(destinationName);
+ if (!destDir.exists()) {
+ destDir.mkdir(destinationName);
+ } else if (!merge) {
+ return false;
+ }
+
+ QStringList files = sourceDir.entryList(QDir::Files);
+ foreach (QString fileName, files) {
+ QString srcName = sourceName + "/" + fileName;
+ QString destName = destinationName + "/" + fileName;
+ QFile::copy(srcName, destName);
+ }
+
+ files.clear();
+ // we leave out symlinks because that could cause an endless recursion
+ QStringList subDirs =
+ sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
+ foreach (QString subDir, subDirs) {
+ QString srcName = sourceName + "/" + subDir;
+ QString destName = destinationName + "/" + subDir;
+ copyDir(srcName, destName, merge);
+ }
+ return true;
+}
+
+// Linux shell operations use QFile/QDir instead of SHFileOperation
+
+static bool shellOpCopy(const QStringList& sourceNames,
+ const QStringList& destinationNames)
+{
+ // Multiple sources → single destination: treat destination as a directory
+ if (destinationNames.count() == 1 && sourceNames.count() > 1) {
+ QDir destDir(destinationNames[0]);
+ if (!destDir.exists()) {
+ destDir.mkpath(".");
+ }
+ for (const auto& src : sourceNames) {
+ QFileInfo srcInfo(src);
+ QString dest = destinationNames[0] + "/" + srcInfo.fileName();
+ if (!QFile::copy(src, dest)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // 1:1 or N:N — direct file-to-file copy
+ if (destinationNames.count() != sourceNames.count()) {
+ return false;
+ }
+
+ for (int i = 0; i < sourceNames.count(); ++i) {
+ QFileInfo destInfo(destinationNames[i]);
+ if (!destInfo.dir().exists()) {
+ destInfo.dir().mkpath(".");
+ }
+ if (!QFile::copy(sourceNames[i], destinationNames[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool shellOpMove(const QStringList& sourceNames,
+ const QStringList& destinationNames)
+{
+ // Multiple sources → single destination: treat destination as a directory
+ if (destinationNames.count() == 1 && sourceNames.count() > 1) {
+ QDir destDir(destinationNames[0]);
+ if (!destDir.exists()) {
+ destDir.mkpath(".");
+ }
+ for (const auto& src : sourceNames) {
+ QFileInfo srcInfo(src);
+ QString dest = destinationNames[0] + "/" + srcInfo.fileName();
+ if (!QFile::rename(src, dest)) {
+ if (!QFile::copy(src, dest) || !QFile::remove(src)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // 1:1 or N:N — direct file-to-file move
+ if (destinationNames.count() != sourceNames.count()) {
+ return false;
+ }
+
+ for (int i = 0; i < sourceNames.count(); ++i) {
+ QFileInfo destInfo(destinationNames[i]);
+ if (!destInfo.dir().exists()) {
+ destInfo.dir().mkpath(".");
+ }
+ if (!QFile::rename(sourceNames[i], destinationNames[i])) {
+ if (!QFile::copy(sourceNames[i], destinationNames[i]) ||
+ !QFile::remove(sourceNames[i])) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool shellOpDelete(const QStringList& fileNames, bool recycle)
+{
+ // On Linux, "recycle" moves to trash using Qt; otherwise just delete
+ for (const auto& fileName : fileNames) {
+ QFileInfo fi(fileName);
+ if (fi.isDir()) {
+ if (!removeDir(fileName)) {
+ return false;
+ }
+ } else {
+ if (recycle) {
+ if (!QFile::moveToTrash(fileName)) {
+ return false;
+ }
+ } else {
+ if (!QFile::remove(fileName)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool shellCopy(const QStringList& sourceNames, const QStringList& destinationNames,
+ QWidget* dialog)
+{
+ (void)dialog;
+ return shellOpCopy(sourceNames, destinationNames);
+}
+
+bool shellCopy(const QString& sourceNames, const QString& destinationNames,
+ bool yesToAll, QWidget* dialog)
+{
+ (void)yesToAll;
+ (void)dialog;
+ return shellOpCopy(QStringList() << sourceNames, QStringList() << destinationNames);
+}
+
+bool shellMove(const QStringList& sourceNames, const QStringList& destinationNames,
+ QWidget* dialog)
+{
+ (void)dialog;
+ return shellOpMove(sourceNames, destinationNames);
+}
+
+bool shellMove(const QString& sourceNames, const QString& destinationNames,
+ bool yesToAll, QWidget* dialog)
+{
+ (void)yesToAll;
+ (void)dialog;
+ return shellOpMove(QStringList() << sourceNames, QStringList() << destinationNames);
+}
+
+bool shellRename(const QString& oldName, const QString& newName, bool yesToAll,
+ QWidget* dialog)
+{
+ (void)yesToAll;
+ (void)dialog;
+ return QFile::rename(oldName, newName);
+}
+
+bool shellDelete(const QStringList& fileNames, bool recycle, QWidget* dialog)
+{
+ (void)dialog;
+ return shellOpDelete(fileNames, recycle);
+}
+
+namespace shell
+{
+
+ static QString g_urlHandler;
+
+ Result::Result(bool success, DWORD error, QString message, HANDLE process)
+ : m_success(success), m_error(error), m_message(std::move(message)),
+ m_process(process)
+ {
+ if (m_message.isEmpty()) {
+ m_message = QString::fromStdWString(formatSystemMessage(m_error));
+ }
+ }
+
+ Result Result::makeFailure(DWORD error, QString message)
+ {
+ return Result(false, error, std::move(message), INVALID_HANDLE_VALUE);
+ }
+
+ Result Result::makeSuccess(HANDLE process)
+ {
+ return Result(true, ERROR_SUCCESS, {}, process);
+ }
+
+ bool Result::success() const
+ {
+ return m_success;
+ }
+
+ Result::operator bool() const
+ {
+ return m_success;
+ }
+
+ DWORD Result::error()
+ {
+ return m_error;
+ }
+
+ const QString& Result::message() const
+ {
+ return m_message;
+ }
+
+ HANDLE Result::processHandle() const
+ {
+ return m_process.get();
+ }
+
+ HANDLE Result::stealProcessHandle()
+ {
+ const auto h = m_process.release();
+ m_process.reset(INVALID_HANDLE_VALUE);
+ return h;
+ }
+
+ QString Result::toString() const
+ {
+ if (m_message.isEmpty()) {
+ return QObject::tr("Error %1").arg(m_error);
+ } else {
+ return m_message;
+ }
+ }
+
+ QString formatError(int i)
+ {
+ switch (i) {
+ case 0:
+ return "The operating system is out of memory or resources";
+ case (int)ERROR_FILE_NOT_FOUND:
+ return "The specified file was not found";
+ case (int)ERROR_PATH_NOT_FOUND:
+ return "The specified path was not found";
+ case (int)ERROR_BAD_FORMAT:
+ return "The .exe file is invalid (non-Win32 .exe or error in .exe image)";
+ case SE_ERR_ACCESSDENIED:
+ return "The operating system denied access to the specified file";
+ case SE_ERR_ASSOCINCOMPLETE:
+ return "The file name association is incomplete or invalid";
+ case SE_ERR_DDEBUSY:
+ return "The DDE transaction could not be completed because other DDE "
+ "transactions were being processed";
+ case SE_ERR_DDEFAIL:
+ return "The DDE transaction failed";
+ case SE_ERR_DDETIMEOUT:
+ return "The DDE transaction could not be completed because the request "
+ "timed out";
+ case SE_ERR_DLLNOTFOUND:
+ return "The specified DLL was not found";
+ case SE_ERR_NOASSOC:
+ return "There is no application associated with the given file name "
+ "extension";
+ case SE_ERR_OOM:
+ return "There was not enough memory to complete the operation";
+ case SE_ERR_SHARE:
+ return "A sharing violation occurred";
+ default:
+ return QString("Unknown error %1").arg(i);
+ }
+ }
+
+ Result ExploreDirectory(const QFileInfo& info)
+ {
+ const auto path = info.absoluteFilePath();
+ // Use xdg-open on Linux
+ if (QProcess::startDetached("xdg-open", {path})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open directory"));
+ }
+
+ Result ExploreFileInDirectory(const QFileInfo& info)
+ {
+ // Try to use the system file manager to highlight the file
+ // dbus method for Nautilus/Files, fallback to xdg-open on parent dir
+ const auto dir = info.absolutePath();
+ if (QProcess::startDetached("xdg-open", {dir})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file manager"));
+ }
+
+ Result Explore(const QFileInfo& info)
+ {
+ if (info.isFile()) {
+ return ExploreFileInDirectory(info);
+ } else if (info.isDir()) {
+ return ExploreDirectory(info);
+ } else {
+ const auto parent = info.dir();
+ if (parent.exists()) {
+ return ExploreDirectory(QFileInfo(parent.absolutePath()));
+ } else {
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND);
+ }
+ }
+ }
+
+ Result Explore(const QString& path)
+ {
+ return Explore(QFileInfo(path));
+ }
+
+ Result Explore(const QDir& dir)
+ {
+ return Explore(QFileInfo(dir.absolutePath()));
+ }
+
+ Result Open(const QString& path)
+ {
+ if (QProcess::startDetached("xdg-open", {path})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file"));
+ }
+
+ Result Open(const QUrl& url)
+ {
+ log::debug("opening url '{}'", url.toString());
+
+ if (g_urlHandler.isEmpty()) {
+ if (QProcess::startDetached("xdg-open", {url.toString()})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open URL"));
+ } else {
+ // Custom URL handler
+ QString cmd = g_urlHandler;
+ cmd.replace("%1", url.toString());
+ if (QProcess::startDetached("/bin/sh", {"-c", cmd})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND,
+ QObject::tr("You have an invalid custom browser command in the settings."));
+ }
+ }
+
+ Result Execute(const QString& program, const QString& params)
+ {
+ QStringList args;
+ if (!params.isEmpty()) {
+ args = QProcess::splitCommand(params);
+ }
+ if (QProcess::startDetached(program, args)) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to execute program"));
+ }
+
+ void SetUrlHandler(const QString& cmd)
+ {
+ g_urlHandler = cmd;
+ }
+
+ Result Delete(const QFileInfo& path)
+ {
+ if (QFile::remove(path.absoluteFilePath())) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_ACCESS_DENIED);
+ }
+
+ Result Rename(const QFileInfo& src, const QFileInfo& dest)
+ {
+ return Rename(src, dest, true);
+ }
+
+ Result Rename(const QFileInfo& src, const QFileInfo& dest, bool copyAllowed)
+ {
+ if (QFile::rename(src.absoluteFilePath(), dest.absoluteFilePath())) {
+ return Result::makeSuccess();
+ }
+
+ if (copyAllowed) {
+ if (QFile::copy(src.absoluteFilePath(), dest.absoluteFilePath())) {
+ QFile::remove(src.absoluteFilePath());
+ return Result::makeSuccess();
+ }
+ }
+
+ return Result::makeFailure(ERROR_ACCESS_DENIED);
+ }
+
+ Result CreateDirectories(const QDir& dir)
+ {
+ if (dir.mkpath(".")) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_ACCESS_DENIED,
+ QObject::tr("Failed to create directory: %1").arg(dir.path()));
+ }
+
+ Result DeleteDirectoryRecursive(const QDir& dir)
+ {
+ if (shellOpDelete({dir.path()}, false)) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_ACCESS_DENIED);
+ }
+
+} // namespace shell
+
+bool moveFileRecursive(const QString& source, const QString& baseDir,
+ const QString& destination)
+{
+ QStringList pathComponents = destination.split("/");
+ QString path = baseDir;
+ for (QStringList::Iterator iter = pathComponents.begin();
+ iter != pathComponents.end() - 1; ++iter) {
+ path.append("/").append(*iter);
+ if (!QDir(path).exists() && !QDir().mkdir(path)) {
+ reportError(QObject::tr("failed to create directory \"%1\"").arg(path));
+ return false;
+ }
+ }
+
+ QString destinationAbsolute = baseDir.mid(0).append("/").append(destination);
+ if (!QFile::rename(source, destinationAbsolute)) {
+ // move failed, try copy & delete
+ if (!QFile::copy(source, destinationAbsolute)) {
+ reportError(QObject::tr("failed to copy \"%1\" to \"%2\"")
+ .arg(source)
+ .arg(destinationAbsolute));
+ return false;
+ } else {
+ QFile::remove(source);
+ }
+ }
+ return true;
+}
+
+bool copyFileRecursive(const QString& source, const QString& baseDir,
+ const QString& destination)
+{
+ QStringList pathComponents = destination.split("/");
+ QString path = baseDir;
+ for (QStringList::Iterator iter = pathComponents.begin();
+ iter != pathComponents.end() - 1; ++iter) {
+ path.append("/").append(*iter);
+ if (!QDir(path).exists() && !QDir().mkdir(path)) {
+ reportError(QObject::tr("failed to create directory \"%1\"").arg(path));
+ return false;
+ }
+ }
+
+ QString destinationAbsolute = baseDir.mid(0).append("/").append(destination);
+ if (!QFile::copy(source, destinationAbsolute)) {
+ reportError(QObject::tr("failed to copy \"%1\" to \"%2\"")
+ .arg(source)
+ .arg(destinationAbsolute));
+ return false;
+ }
+ return true;
+}
+
+std::wstring ToWString(const QString& source)
+{
+ return source.toStdWString();
+}
+
+std::string ToString(const QString& source, bool utf8)
+{
+ QByteArray array8bit;
+ if (utf8) {
+ array8bit = source.toUtf8();
+ } else {
+ array8bit = source.toLocal8Bit();
+ }
+ return std::string(array8bit.constData());
+}
+
+QString ToQString(const std::string& source)
+{
+ return QString::fromStdString(source);
+}
+
+QString ToQString(const std::wstring& source)
+{
+ return QString::fromStdWString(source);
+}
+
+bool isWindowsDrivePath(const QString& path)
+{
+ static const QRegularExpression re(R"(^\s*[A-Za-z]:[\\/].*)");
+ return re.match(path).hasMatch();
+}
+
+bool isWineZDrivePath(const QString& path)
+{
+ static const QRegularExpression re(R"(^\s*[Zz]:[\\/].*)");
+ return re.match(path).hasMatch();
+}
+
+QString toWinePath(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ QString p = path.trimmed();
+ if (p.isEmpty()) {
+ return p;
+ }
+
+ if (isWindowsDrivePath(p)) {
+ p.replace('/', '\\');
+ return p;
+ }
+
+ // Expand "~" on Linux before mapping to Z:.
+ if (p.startsWith("~")) {
+ p.replace(0, 1, QDir::homePath());
+ }
+
+ if (!QDir::isAbsolutePath(p)) {
+ p = QDir(p).absolutePath();
+ }
+
+ p = QDir::cleanPath(p);
+ p.replace('/', '\\');
+ return "Z:" + p;
+#endif
+}
+
+QString fromWinePath(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ QString p = path.trimmed();
+ if (p.isEmpty()) {
+ return p;
+ }
+
+ if (!isWindowsDrivePath(p)) {
+ return QDir::cleanPath(p);
+ }
+
+ // We only map Wine's Z: drive to host Linux absolute paths.
+ if (!isWineZDrivePath(p)) {
+ return p;
+ }
+
+ p = p.mid(2); // strip "Z:"
+ p.replace('\\', '/');
+ if (!p.startsWith('/')) {
+ p.prepend('/');
+ }
+
+ return QDir::cleanPath(p);
+#endif
+}
+
+QString normalizePathForHost(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ if (isWineZDrivePath(path)) {
+ return fromWinePath(path);
+ }
+ return QDir::cleanPath(path);
+#endif
+}
+
+QString normalizePathForWine(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ if (isWindowsDrivePath(path)) {
+ QString p = path;
+ p.replace('/', '\\');
+ return p;
+ }
+ return toWinePath(path);
+#endif
+}
+
+#ifdef _WIN32
+QString ToString(const SYSTEMTIME& time)
+{
+ char dateBuffer[100];
+ char timeBuffer[100];
+ int size = 100;
+ GetDateFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, dateBuffer,
+ size);
+ GetTimeFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, timeBuffer,
+ size);
+ return QString::fromLocal8Bit(dateBuffer) + " " + QString::fromLocal8Bit(timeBuffer);
+}
+#endif
+
+static int naturalCompareI(const QString& a, const QString& b)
+{
+ static QCollator c = [] {
+ QCollator temp;
+ temp.setNumericMode(true);
+ temp.setCaseSensitivity(Qt::CaseInsensitive);
+ return temp;
+ }();
+
+ return c.compare(a, b);
+}
+
+int naturalCompare(const QString& a, const QString& b, Qt::CaseSensitivity cs)
+{
+ if (cs == Qt::CaseInsensitive) {
+ return naturalCompareI(a, b);
+ }
+
+ static QCollator c = [] {
+ QCollator temp;
+ temp.setNumericMode(true);
+ return temp;
+ }();
+
+ return c.compare(a, b);
+}
+
+#ifdef _WIN32
+struct CoTaskMemFreer
+{
+ void operator()(void* p) { ::CoTaskMemFree(p); }
+};
+
+template <class T>
+using COMMemPtr = std::unique_ptr<T, CoTaskMemFreer>;
+
+QString getOptionalKnownFolder(KNOWNFOLDERID id)
+{
+ COMMemPtr<wchar_t> path;
+ {
+ wchar_t* rawPath = nullptr;
+ HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath);
+ if (FAILED(res)) {
+ return {};
+ }
+ path.reset(rawPath);
+ }
+ return QString::fromWCharArray(path.get());
+}
+
+QDir getKnownFolder(KNOWNFOLDERID id, const QString& what)
+{
+ COMMemPtr<wchar_t> path;
+ {
+ wchar_t* rawPath = nullptr;
+ HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath);
+ if (FAILED(res)) {
+ log::error("failed to get known folder '{}', {}",
+ what.isEmpty() ? QUuid(id).toString() : what,
+ formatSystemMessage(res));
+ throw std::runtime_error("couldn't get known folder path");
+ }
+ path.reset(rawPath);
+ }
+ return QString::fromWCharArray(path.get());
+}
+#endif
+
+QString getDesktopDirectory()
+{
+#ifdef _WIN32
+ return getKnownFolder(FOLDERID_Desktop, "desktop").absolutePath();
+#else
+ return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
+#endif
+}
+
+QString getStartMenuDirectory()
+{
+#ifdef _WIN32
+ return getKnownFolder(FOLDERID_StartMenu, "start menu").absolutePath();
+#else
+ return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
+#endif
+}
+
+bool shellDeleteQuiet(const QString& fileName, QWidget* dialog)
+{
+ if (!QFile::remove(fileName)) {
+ return shellDelete(QStringList(fileName), false, dialog);
+ }
+ return true;
+}
+
+QString readFileText(const QString& fileName, QString* encoding, bool* hadBOM)
+{
+
+ QFile textFile(fileName);
+ if (!textFile.open(QIODevice::ReadOnly)) {
+ return QString();
+ }
+
+ QByteArray buffer = textFile.readAll();
+ return decodeTextData(buffer, encoding, hadBOM);
+}
+
+QString decodeTextData(const QByteArray& fileData, QString* encoding, bool* hadBOM)
+{
+ QStringConverter::Encoding codec = QStringConverter::Encoding::Utf8;
+ QStringEncoder encoder(codec);
+ QStringDecoder decoder(codec, QStringConverter::Flag::ConvertInitialBom);
+ QString text = decoder.decode(fileData);
+
+ // embedded nulls probably mean it was UTF-16 - they're rare/illegal in text files
+ bool hasEmbeddedNulls = false;
+ for (const auto& character : text) {
+ if (character.isNull()) {
+ hasEmbeddedNulls = true;
+ break;
+ }
+ }
+
+ // check reverse conversion
+ if (hasEmbeddedNulls || encoder.encode(text) != fileData) {
+ log::debug("conversion failed assuming local encoding");
+ auto codecSearch = QStringConverter::encodingForData(fileData);
+ if (codecSearch.has_value()) {
+ codec = codecSearch.value();
+ decoder = QStringDecoder(codec, QStringConverter::Flag::ConvertInitialBom);
+ } else {
+ decoder = QStringDecoder(hasEmbeddedNulls ? QStringConverter::Encoding::Utf16
+ : QStringConverter::Encoding::System);
+ }
+ text = decoder.decode(fileData);
+ }
+
+ if (encoding != nullptr) {
+ *encoding = QStringConverter::nameForEncoding(codec);
+ }
+
+ if (!text.isEmpty() && text.startsWith(QChar::ByteOrderMark)) {
+ text.remove(0, 1);
+
+ if (hadBOM != nullptr) {
+ *hadBOM = true;
+ }
+ } else if (hadBOM != nullptr) {
+ *hadBOM = false;
+ }
+
+ return text;
+}
+
+void removeOldFiles(const QString& path, const QString& pattern, int numToKeep,
+ QDir::SortFlags sorting)
+{
+ QFileInfoList files =
+ QDir(path).entryInfoList(QStringList(pattern), QDir::Files, sorting);
+
+ if (files.count() > numToKeep) {
+ QStringList deleteFiles;
+ for (int i = 0; i < files.count() - numToKeep; ++i) {
+ deleteFiles.append(files.at(i).absoluteFilePath());
+ }
+
+ if (!shellDelete(deleteFiles)) {
+ log::warn("failed to remove log files");
+ }
+ }
+}
+
+QIcon iconForExecutable(const QString& filePath)
+{
+ static QHash<QString, QIcon> cache;
+
+ const QFileInfo fi(filePath);
+ if (!fi.exists()) {
+ return QIcon(":/MO/gui/executable");
+ }
+
+ const QString cacheKey =
+ fi.canonicalFilePath() + "|" + QString::number(fi.size()) + "|" +
+ fi.lastModified().toString(Qt::ISODateWithMs);
+
+ if (cache.contains(cacheKey)) {
+ return cache.value(cacheKey);
+ }
+
+#ifdef _WIN32
+ QIcon icon(":/MO/gui/executable");
+ cache.insert(cacheKey, icon);
+ return icon;
+#else
+ // Try to extract icon resources from PE executables via icoutils/wrestool.
+ // If this fails, return a generic executable icon.
+ QIcon icon;
+
+ const QString wrestool = QStandardPaths::findExecutable("wrestool");
+ if (!wrestool.isEmpty()) {
+ const QString cacheRoot =
+ QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
+ "/exe_icons";
+ QDir().mkpath(cacheRoot);
+
+ const QString outDirPath = cacheRoot + "/" + QString::number(qHash(cacheKey), 16);
+ QDir outDir(outDirPath);
+ if (!outDir.exists()) {
+ QDir().mkpath(outDirPath);
+ }
+
+ auto findBestIcon = [&](const QString& dirPath) -> QIcon {
+ QString bestFile;
+ int bestArea = -1;
+
+ QDirIterator it(dirPath,
+ QStringList() << "*.png"
+ << "*.ico"
+ << "*.bmp",
+ QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ const QString path = it.next();
+ QImageReader reader(path);
+ const QSize size = reader.size();
+ const int area = size.width() * size.height();
+ if (area > bestArea) {
+ bestArea = area;
+ bestFile = path;
+ } else if (bestArea < 0) {
+ // Unknown image size (e.g. multi-size ico); keep first viable file.
+ bestFile = path;
+ }
+ }
+
+ if (!bestFile.isEmpty()) {
+ QIcon candidate(bestFile);
+ if (!candidate.pixmap(24, 24).isNull() || !candidate.pixmap(32, 32).isNull()) {
+ return candidate;
+ }
+ }
+
+ return QIcon();
+ };
+
+ // Reuse previous extraction if present.
+ icon = findBestIcon(outDirPath);
+
+ if (icon.isNull()) {
+ const QList<QStringList> tries = {
+ {"-x", "--type=group_icon", "-o", outDirPath, fi.absoluteFilePath()},
+ {"-x", "--type=14", "-o", outDirPath, fi.absoluteFilePath()},
+ {"-x", "-t14", "-o", outDirPath, fi.absoluteFilePath()},
+ };
+
+ for (const auto& args : tries) {
+ QProcess p;
+ p.start(wrestool, args);
+ if (!p.waitForFinished(4000)) {
+ p.kill();
+ continue;
+ }
+ if (p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0) {
+ icon = findBestIcon(outDirPath);
+ if (!icon.isNull()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (icon.isNull()) {
+ icon = QIcon(":/MO/gui/executable");
+ }
+
+ cache.insert(cacheKey, icon);
+ return icon;
+#endif
+}
+
+QString getFileVersion(QString const& filepath)
+{
+ // On Linux, there is no Windows PE file version. Return empty.
+ (void)filepath;
+ return "";
+}
+
+QString getProductVersion(QString const& filepath)
+{
+ // On Linux, there is no Windows PE product version. Return empty.
+ (void)filepath;
+ return "";
+}
+
+void deleteChildWidgets(QWidget* w)
+{
+ auto* ly = w->layout();
+ if (!ly) {
+ return;
+ }
+
+ while (auto* item = ly->takeAt(0)) {
+ delete item->widget();
+ delete item;
+ }
+}
+
+void trimWString(std::wstring& s)
+{
+ s.erase(std::remove_if(s.begin(), s.end(),
+ [](wint_t ch) {
+ return std::iswspace(ch);
+ }),
+ s.end());
+}
+
+std::wstring formatMessage(DWORD id, const std::wstring& message)
+{
+ std::wstring s;
+
+ std::wostringstream oss;
+ oss << L"0x" << std::hex << id;
+
+ if (message.empty()) {
+ s = oss.str();
+ } else {
+ s += message + L" (" + oss.str() + L")";
+ }
+
+ return s;
+}
+
+std::wstring formatSystemMessage(DWORD id)
+{
+#ifdef _WIN32
+ std::wstring getMessage(DWORD id, HMODULE mod);
+ return formatMessage(id, getMessage(id, 0));
+#else
+ // On Linux, map common error codes to strings or use strerror for errno values
+ if (id == 0) {
+ return L"Success";
+ }
+ // If it looks like an errno value (small numbers), use strerror
+ if (id < 200) {
+ const char* msg = strerror(static_cast<int>(id));
+ if (msg) {
+ std::string s(msg);
+ return std::wstring(s.begin(), s.end());
+ }
+ }
+ return formatMessage(id, L"");
+#endif
+}
+
+std::wstring formatNtMessage(NTSTATUS s)
+{
+#ifdef _WIN32
+ const DWORD id = static_cast<DWORD>(s);
+ std::wstring getMessage(DWORD id, HMODULE mod);
+ return formatMessage(id, getMessage(id, ::GetModuleHandleW(L"ntdll.dll")));
+#else
+ return formatMessage(static_cast<DWORD>(s), L"NT status");
+#endif
+}
+
+QString windowsErrorString(DWORD errorCode)
+{
+ return QString::fromStdWString(formatSystemMessage(errorCode));
+}
+
+QString localizedSize(unsigned long long bytes, const QString& B, const QString& KB,
+ const QString& MB, const QString& GB, const QString& TB)
+{
+ constexpr unsigned long long OneKB = 1024ull;
+ constexpr unsigned long long OneMB = 1024ull * 1024;
+ constexpr unsigned long long OneGB = 1024ull * 1024 * 1024;
+ constexpr unsigned long long OneTB = 1024ull * 1024 * 1024 * 1024;
+
+ auto makeNum = [&](int factor) {
+ const double n = static_cast<double>(bytes) / std::pow(1024.0, factor);
+ const double truncated =
+ static_cast<double>(static_cast<unsigned long long>(n * 100)) / 100.0;
+ return QString().setNum(truncated, 'f', 2);
+ };
+
+ if (bytes < OneKB) {
+ return B.arg(bytes);
+ } else if (bytes < OneMB) {
+ return KB.arg(makeNum(1));
+ } else if (bytes < OneGB) {
+ return MB.arg(makeNum(2));
+ } else if (bytes < OneTB) {
+ return GB.arg(makeNum(3));
+ } else {
+ return TB.arg(makeNum(4));
+ }
+}
+
+QDLLEXPORT QString localizedByteSize(unsigned long long bytes)
+{
+ return localizedSize(bytes, QObject::tr("%1 B"), QObject::tr("%1 KB"),
+ QObject::tr("%1 MB"), QObject::tr("%1 GB"),
+ QObject::tr("%1 TB"));
+}
+
+QDLLEXPORT QString localizedByteSpeed(unsigned long long bps)
+{
+ return localizedSize(bps, QObject::tr("%1 B/s"), QObject::tr("%1 KB/s"),
+ QObject::tr("%1 MB/s"), QObject::tr("%1 GB/s"),
+ QObject::tr("%1 TB/s"));
+}
+
+QDLLEXPORT QString localizedTimeRemaining(unsigned int remaining)
+{
+ QString Result;
+ double interval;
+ qint64 intval;
+
+ // Hours
+ interval = 60.0 * 60.0 * 1000.0;
+ intval = (qint64)trunc((double)remaining / interval);
+ if (intval < 0)
+ intval = 0;
+ remaining -= static_cast<unsigned int>(trunc((double)intval * interval));
+ qint64 hours = intval;
+
+ // Minutes
+ interval = 60.0 * 1000.0;
+ intval = (qint64)trunc((double)remaining / interval);
+ if (intval < 0)
+ intval = 0;
+ remaining -= static_cast<unsigned int>(trunc((double)intval * interval));
+ qint64 minutes = intval;
+
+ // Seconds
+ interval = 1000.0;
+ intval = (qint64)trunc((double)remaining / interval);
+ if (intval < 0)
+ intval = 0;
+ remaining -= static_cast<unsigned int>(trunc((double)intval * interval));
+ qint64 seconds = intval;
+
+ char buffer[25];
+ memset(buffer, 0, 25);
+
+ if (hours > 0) {
+ if (hours < 10)
+ snprintf(buffer, sizeof(buffer), "0%lld", (long long)hours);
+ else
+ snprintf(buffer, sizeof(buffer), "%lld", (long long)hours);
+ Result.append(QString("%1:").arg(buffer));
+ }
+
+ if (minutes > 0 || hours > 0) {
+ if (minutes < 10 && hours > 0)
+ snprintf(buffer, sizeof(buffer), "0%lld", (long long)minutes);
+ else
+ snprintf(buffer, sizeof(buffer), "%lld", (long long)minutes);
+ Result.append(QString("%1:").arg(buffer));
+ }
+
+ if (seconds < 10 && (minutes > 0 || hours > 0))
+ snprintf(buffer, sizeof(buffer), "0%lld", (long long)seconds);
+ else
+ snprintf(buffer, sizeof(buffer), "%lld", (long long)seconds);
+ Result.append(QString("%1").arg(buffer));
+
+ if (hours > 0)
+ //: Time remaining hours
+ Result.append(QApplication::translate("uibase", "h"));
+ else if (minutes > 0)
+ //: Time remaining minutes
+ Result.append(QApplication::translate("uibase", "m"));
+ else
+ //: Time remaining seconds
+ Result.append(QApplication::translate("uibase", "s"));
+
+ return Result;
+}
+
+QDLLEXPORT void localizedByteSizeTests()
+{
+ auto f = [](unsigned long long n) {
+ return localizedByteSize(n).toStdString();
+ };
+
+#define CHECK_EQ(a, b) \
+ if ((a) != (b)) { \
+ std::cerr << "failed: " << a << " == " << b << "\n"; \
+ }
+
+ CHECK_EQ(f(0), "0 B");
+ CHECK_EQ(f(1), "1 B");
+ CHECK_EQ(f(999), "999 B");
+ CHECK_EQ(f(1000), "1000 B");
+ CHECK_EQ(f(1023), "1023 B");
+
+ CHECK_EQ(f(1024), "1.00 KB");
+ CHECK_EQ(f(2047), "1.99 KB");
+ CHECK_EQ(f(2048), "2.00 KB");
+ CHECK_EQ(f(1048575), "1023.99 KB");
+
+ CHECK_EQ(f(1048576), "1.00 MB");
+ CHECK_EQ(f(1073741823), "1023.99 MB");
+
+ CHECK_EQ(f(1073741824), "1.00 GB");
+ CHECK_EQ(f(1099511627775), "1023.99 GB");
+
+ CHECK_EQ(f(1099511627776), "1.00 TB");
+ CHECK_EQ(f(2759774185818), "2.51 TB");
+
+#undef CHECK_EQ
+}
+
+TimeThis::TimeThis(const QString& what) : m_running(false)
+{
+ start(what);
+}
+
+TimeThis::~TimeThis()
+{
+ stop();
+}
+
+void TimeThis::start(const QString& what)
+{
+ stop();
+
+ m_what = what;
+ m_start = Clock::now();
+ m_running = true;
+}
+
+void TimeThis::stop()
+{
+ using namespace std::chrono;
+
+ if (!m_running) {
+ return;
+ }
+
+ const auto end = Clock::now();
+ const auto d = duration_cast<milliseconds>(end - m_start).count();
+
+ if (m_what.isEmpty()) {
+ log::debug("timing: {} ms", d);
+ } else {
+ log::debug("timing: {} {} ms", m_what, d);
+ }
+
+ m_running = false;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/version.rc b/libs/uibase/src/version.rc
new file mode 100644
index 0000000..fdea5f9
--- /dev/null
+++ b/libs/uibase/src/version.rc
@@ -0,0 +1,37 @@
+#include "Winver.h"
+
+// 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.3
+#define VER_FILEVERSION_STR "2.5.3\0"
+
+VS_VERSION_INFO VERSIONINFO
+FILEVERSION VER_FILEVERSION
+PRODUCTVERSION VER_FILEVERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+FILEFLAGS (0)
+FILEOS VOS__WINDOWS32
+FILETYPE VFT_APP
+FILESUBTYPE (0)
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904B0"
+ BEGIN
+ VALUE "FileVersion", VER_FILEVERSION_STR
+ VALUE "CompanyName", "Mod Organizer 2 Team\0"
+ VALUE "FileDescription", "MO2 Base UI plugin API library\0"
+ VALUE "OriginalFilename", "uibase.dll\0"
+ VALUE "InternalName", "uibase\0"
+ VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2026 Mod Organizer 2 contributors\0"
+ VALUE "ProductName", "Mod Organizer 2 UI Base\0"
+ VALUE "ProductVersion", VER_FILEVERSION_STR
+ END
+ END
+
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409, 1200
+ END
+END
diff --git a/libs/uibase/src/versioninfo.cpp b/libs/uibase/src/versioninfo.cpp
new file mode 100644
index 0000000..6dec18e
--- /dev/null
+++ b/libs/uibase/src/versioninfo.cpp
@@ -0,0 +1,432 @@
+/*
+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
+*/
+
+#include <uibase/versioninfo.h>
+#include <QDate>
+#include <QLocale>
+#include <QRegularExpression>
+#include <QVersionNumber>
+
+namespace
+{
+const QRegularExpression VERSION_REGEX("^(\\d+)(\\.(\\d+))?(\\.(\\d+))?(\\.(\\d+))?");
+}
+
+namespace MOBase
+{
+
+VersionInfo::VersionInfo()
+ : m_Scheme(SCHEME_REGULAR), m_Valid(false), m_ReleaseType(RELEASE_FINAL),
+ m_Major(0), m_Minor(0), m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0),
+ m_Rest()
+{}
+
+VersionInfo::VersionInfo(int major, int minor, int subminor, int subsubminor,
+ ReleaseType releaseType)
+ : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType),
+ m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(subsubminor),
+ m_DecimalPositions(0), m_Rest()
+{}
+
+VersionInfo::VersionInfo(int major, int minor, int subminor, ReleaseType releaseType)
+ : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType),
+ m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(0),
+ m_DecimalPositions(0), m_Rest()
+{}
+
+VersionInfo::VersionInfo(const QString& versionString, VersionScheme scheme)
+ : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0),
+ m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest()
+{
+ parse(versionString, scheme);
+}
+
+VersionInfo::VersionInfo(const QString& versionString,
+ VersionInfo::VersionScheme scheme, bool manualInput)
+ : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0),
+ m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest()
+{
+ parse(versionString, scheme, manualInput);
+}
+
+void VersionInfo::clear()
+{
+ m_Scheme = SCHEME_REGULAR;
+ m_Valid = false;
+ m_ReleaseType = RELEASE_FINAL;
+ m_Major = m_Minor = m_SubMinor = m_SubSubMinor = m_DecimalPositions = 0;
+ m_Rest.clear();
+}
+
+QString VersionInfo::canonicalString() const
+{
+ if (!isValid()) {
+ return QString();
+ }
+
+ QString result;
+ if (m_Scheme == SCHEME_REGULAR) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DECIMALMARK) {
+ result = QString("f%1.%2").arg(m_Major).arg(
+ QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0'));
+ } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) {
+ result = QString("n%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DATE) {
+ // year.month.day was stored in the version fields
+ result = QString("d%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ }
+ switch (m_ReleaseType) {
+ case RELEASE_PREALPHA: {
+ result.append(" pre-alpha");
+ } break;
+ case RELEASE_ALPHA: {
+ result.append("a");
+ } break;
+ case RELEASE_BETA: {
+ result.append("b");
+ } break;
+ case RELEASE_CANDIDATE: {
+ result.append("rc");
+ } break;
+ case RELEASE_FINAL: // fall-through
+ default: {
+ // nop
+ } break;
+ }
+
+ if (!m_Rest.isEmpty()) {
+ result.append(QString("%1").arg(m_Rest));
+ }
+
+ return result;
+}
+
+QString VersionInfo::displayString(int forcedVersionSegments) const
+{
+ if (!isValid()) {
+ return QString();
+ }
+
+ QString result;
+ if (m_Scheme == SCHEME_REGULAR) {
+ if (forcedVersionSegments >= 4 || m_SubSubMinor != 0) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (forcedVersionSegments == 3 || m_SubMinor != 0) {
+ result = QString("%1.%2.%3").arg(m_Major).arg(m_Minor).arg(m_SubMinor);
+ } else {
+ result = QString("%1.%2").arg(m_Major).arg(m_Minor);
+ }
+ } else if (m_Scheme == SCHEME_DECIMALMARK) {
+ result = QString("%1.%2").arg(m_Major).arg(
+ QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0'));
+ } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DATE) {
+ // year.month.day was stored in the version fields
+ const auto year = m_Major;
+ const auto month = m_Minor;
+ const auto day = m_SubMinor;
+
+ return QLocale::system().toString(QDate(year, month, day),
+ QLocale::FormatType::ShortFormat);
+ }
+ switch (m_ReleaseType) {
+ case RELEASE_PREALPHA: {
+ result.append(" pre-alpha");
+ } break;
+ case RELEASE_ALPHA: {
+ result.append("alpha");
+ } break;
+ case RELEASE_BETA: {
+ result.append("beta");
+ } break;
+ case RELEASE_CANDIDATE: {
+ result.append("rc");
+ } break;
+ case RELEASE_FINAL: // fall-through
+ default: {
+ // nop
+ } break;
+ }
+
+ if (!m_Rest.isEmpty()) {
+ result.append(QString("%1").arg(m_Rest));
+ }
+
+ return result;
+}
+
+QVersionNumber VersionInfo::asQVersionNumber() const
+{
+ return QVersionNumber::fromString(displayString()).normalized();
+}
+
+QString VersionInfo::parseReleaseType(QString versionString)
+{
+ // release types are often followed by a number (i.e. "beta4"). This needs to be
+ // extracted now, otherwise the outer parser will think it's the subminor version and
+ // then 1.0.0rc1 would be interpreted as newer than 1.0.0
+
+ static std::map<QString, ReleaseType> typeStrings = {{"prealpha", RELEASE_PREALPHA},
+ {"alpha", RELEASE_ALPHA},
+ {"beta", RELEASE_BETA},
+ {"rc", RELEASE_CANDIDATE}};
+
+ m_ReleaseType = RELEASE_FINAL;
+
+ auto typeIter = typeStrings.begin();
+ int offset = -1;
+
+ for (; (typeIter != typeStrings.end()) && (offset == -1); ++typeIter) {
+ offset = (int)versionString.indexOf(typeIter->first, Qt::CaseInsensitive);
+ if (offset != -1) {
+ m_ReleaseType = typeIter->second;
+ break;
+ }
+ }
+
+ int length = 0;
+
+ if (typeIter != typeStrings.end()) {
+ length = (int)typeIter->first.length();
+ }
+
+ if (m_Scheme == SCHEME_REGULAR) {
+ // also interpret the a/b letters, but only if they follow immediately on the
+ // version number, otherwise the margin for error is too big
+ if ((offset == -1) && (versionString.length() > 0)) {
+ if (versionString.at(0) == 'a') {
+ m_ReleaseType = RELEASE_ALPHA;
+ offset = 0;
+ length = 1;
+ } else if (versionString.at(0) == 'b') {
+ m_ReleaseType = RELEASE_BETA;
+ offset = 0;
+ length = 1;
+ }
+ }
+ }
+
+ if (offset != -1) {
+ versionString.remove(offset, length);
+ }
+ return versionString.trimmed();
+}
+
+void VersionInfo::parse(const QString& versionString, VersionScheme scheme,
+ bool manualInput)
+{
+ m_Valid = false;
+ m_Scheme = scheme == SCHEME_LITERAL ? SCHEME_REGULAR
+ : scheme != SCHEME_DISCOVER ? scheme
+ : SCHEME_REGULAR;
+ m_ReleaseType = RELEASE_FINAL;
+ m_Major = m_Minor = m_SubMinor = m_SubSubMinor = 0;
+ m_Rest.clear();
+ if (versionString.length() == 0) {
+ return;
+ }
+
+ if (QString::compare(versionString, "final", Qt::CaseInsensitive) == 0) {
+ m_Major = 1;
+ m_Valid = true;
+ return;
+ }
+
+ QString temp = versionString;
+ // first, determine the versioning scheme if there is a hint
+ VersionScheme newScheme = m_Scheme;
+ if (!manualInput) {
+ if (temp.startsWith('f')) {
+ newScheme = SCHEME_DECIMALMARK;
+ temp.remove(0, 1);
+ } else if (temp.startsWith('n')) {
+ newScheme = SCHEME_NUMBERSANDLETTERS;
+ temp.remove(0, 1);
+ } else if (temp.startsWith('d')) {
+ newScheme = SCHEME_DATE;
+ temp.remove(0, 1);
+ }
+ }
+
+ if (scheme == SCHEME_DISCOVER) {
+ m_Scheme = newScheme;
+ }
+
+ if (temp.startsWith('v', Qt::CaseInsensitive)) {
+ // v is often prepended to versions
+ temp.remove(0, 1);
+ }
+
+ auto match = VERSION_REGEX.match(temp);
+ if (match.hasMatch()) {
+ m_Major = match.captured(1).toInt();
+ m_Minor = match.captured(3).toInt();
+ QString subMinor = match.captured(5);
+ QString subSubMinor = match.captured(7);
+ if (!subMinor.isEmpty() && (m_Scheme == SCHEME_DECIMALMARK)) {
+ // nooooope, if there are two dots it can't be a decimal mark
+ m_Scheme = SCHEME_REGULAR;
+ }
+ if (m_Scheme != SCHEME_DECIMALMARK) {
+ m_SubMinor = subMinor.toInt();
+ m_SubSubMinor = subSubMinor.toInt();
+ }
+ if (subMinor.isEmpty() && (match.captured(3).size() > 1) &&
+ match.captured(3).startsWith('0')) {
+ // this indicates a decimal scheme
+ m_Scheme = SCHEME_DECIMALMARK;
+ m_DecimalPositions = (int)match.captured(3).size();
+ }
+ temp.remove(VERSION_REGEX);
+ } else {
+ m_Scheme = SCHEME_LITERAL;
+ }
+
+ if (m_Scheme == SCHEME_REGULAR) {
+ temp = parseReleaseType(temp);
+ }
+
+ if ((m_Scheme == SCHEME_DATE) && (m_Major < 1900)) {
+ m_Scheme = SCHEME_REGULAR;
+ }
+ m_Rest = temp.trimmed();
+ m_Valid = true;
+}
+
+QDLLEXPORT bool operator<(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ if (!LHS.isValid() && RHS.isValid())
+ return true;
+ if (!RHS.isValid() && LHS.isValid())
+ return false;
+
+ // date-releases are lower than regular versions
+ if ((LHS.m_Scheme == VersionInfo::SCHEME_DATE) &&
+ (RHS.m_Scheme != VersionInfo::SCHEME_DATE)) {
+ return true;
+ } else if ((LHS.m_Scheme != VersionInfo::SCHEME_DATE) &&
+ (RHS.m_Scheme == VersionInfo::SCHEME_DATE)) {
+ return false;
+ } else if ((LHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK) ||
+ (RHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK)) {
+ // use decimal versioning if either version is a decimal. The parser interprets
+ // versions as regular if in doubt so if the scheme is "decimal" it is definitively
+ // a decimal version number whereas SCHEME_REGULAR means "probably regular"
+
+ float leftVal = QString("%1.%2")
+ .arg(LHS.m_Major)
+ .arg(QString("%1")
+ .arg(LHS.m_Minor)
+ .rightJustified(LHS.m_DecimalPositions, '0'))
+ .toFloat();
+ float rightVal = QString("%1.%2")
+ .arg(RHS.m_Major)
+ .arg(QString("%1")
+ .arg(RHS.m_Minor)
+ .rightJustified(RHS.m_DecimalPositions, '0'))
+ .toFloat();
+ if (fabs(leftVal - rightVal) > 0.001f) {
+ return leftVal < rightVal;
+ }
+ } else {
+ // if in doubt, use the sane choice. regular and numbers+letters can be treated the
+ // same way
+ if (LHS.m_Major != RHS.m_Major)
+ return LHS.m_Major < RHS.m_Major;
+ if (LHS.m_Minor != RHS.m_Minor)
+ return LHS.m_Minor < RHS.m_Minor;
+ if (LHS.m_SubMinor != RHS.m_SubMinor)
+ return LHS.m_SubMinor < RHS.m_SubMinor;
+ if (LHS.m_SubSubMinor != RHS.m_SubSubMinor)
+ return LHS.m_SubSubMinor < RHS.m_SubSubMinor;
+ }
+
+ // subminor, release-type and rest are treated the same for all versioning schemes,
+ // but on parsing they may still differ, i.e. a b-suffix is only interpreted to mean
+ // "beta" in the regular scheme
+ if (LHS.m_ReleaseType != RHS.m_ReleaseType)
+ return LHS.m_ReleaseType < RHS.m_ReleaseType;
+
+ // if the rest contains only integers, compare them numerically
+ bool LHS_ok, RHS_ok;
+ int LHS_int, RHS_int;
+ LHS_int = LHS.m_Rest.toInt(&LHS_ok);
+ RHS_int = RHS.m_Rest.toInt(&RHS_ok);
+ if (LHS_ok && RHS_ok) {
+ return LHS_int < RHS_int;
+ }
+
+ // give up and compare lexically
+ return LHS.m_Rest < RHS.m_Rest;
+}
+
+QDLLEXPORT bool operator>(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return !(LHS <= RHS);
+}
+
+QDLLEXPORT bool operator<=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ // TODO not exactly optimized...
+ if (LHS < RHS) {
+ return true;
+ } else {
+ return !(RHS < LHS);
+ }
+}
+
+QDLLEXPORT bool operator>=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return RHS <= LHS;
+}
+
+QDLLEXPORT bool operator!=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return (LHS < RHS) || (RHS < LHS);
+}
+
+QDLLEXPORT bool operator==(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return !(LHS < RHS) && !(RHS < LHS);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/versioning.cpp b/libs/uibase/src/versioning.cpp
new file mode 100644
index 0000000..8178841
--- /dev/null
+++ b/libs/uibase/src/versioning.cpp
@@ -0,0 +1,278 @@
+#include <uibase/versioning.h>
+
+#include <QRegularExpression>
+#include <format>
+#include <unordered_map>
+
+#include <uibase/formatters.h>
+
+// official semver regex
+static const QRegularExpression s_SemVerStrictRegEx{
+ R"(^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?: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<buildmetadata>[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?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:\.(?P<subpatch>0|[1-9]\d*))?(?:(?P<type>dev|a|alpha|b|beta|rc)(?P<prerelease>0|[1-9](?:[.0-9])*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"};
+
+// match from value to release type
+static const std::unordered_map<QString, MOBase::Version::ReleaseType>
+ 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<std::variant<int, Version::ReleaseType>> 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<std::variant<int, Version::ReleaseType>> 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<std::variant<int, ReleaseType>> 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<decltype(pre), ReleaseType const&>) {
+ 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 <class It>
+ It consumePreReleaseZeros(It it, It end)
+ {
+ for (; it != end; ++it) {
+ if (!std::holds_alternative<int>(*it) != 0 || std::get<int>(*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/libs/uibase/src/widgetutility.cpp b/libs/uibase/src/widgetutility.cpp
new file mode 100644
index 0000000..e3a4b06
--- /dev/null
+++ b/libs/uibase/src/widgetutility.cpp
@@ -0,0 +1,59 @@
+#include <uibase/widgetutility.h>
+#include <uibase/eventfilter.h>
+#include <QCheckBox>
+#include <QHeaderView>
+#include <QMenu>
+#include <QWidgetAction>
+
+namespace MOBase
+{
+
+void onHeaderContextMenu(QTreeView* view, const QPoint& pos)
+{
+ auto* header = view->header();
+
+ QMenu menu;
+
+ // display a list of all headers as checkboxes
+ QAbstractItemModel* model = header->model();
+
+ for (int i = 1; i < model->columnCount(); ++i) {
+ const QString columnName = model->headerData(i, Qt::Horizontal).toString();
+
+ auto* checkBox = new QCheckBox(&menu);
+ checkBox->setText(columnName);
+ checkBox->setChecked(!header->isSectionHidden(i));
+
+ // Enable the checkbox if 1) the section is visible, or 2) the
+ // EnabledColumnRole is not found, or 3) the value for the role is true.
+ auto display = model->headerData(i, Qt::Horizontal, EnabledColumnRole);
+ checkBox->setEnabled(!header->isSectionHidden(i) || !display.isValid() ||
+ display.toBool());
+
+ auto* checkableAction = new QWidgetAction(&menu);
+ checkableAction->setDefaultWidget(checkBox);
+
+ QObject::connect(checkBox, &QCheckBox::clicked, [=] {
+ header->setSectionHidden(i, !checkBox->isChecked());
+ });
+
+ menu.addAction(checkableAction);
+ }
+
+ menu.exec(header->viewport()->mapToGlobal(pos));
+}
+
+void setCustomizableColumns(QTreeView* view)
+{
+ auto* header = view->header();
+
+ header->setSectionsMovable(true);
+ header->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ QObject::connect(header, &QWidget::customContextMenuRequested, view,
+ [view](auto&& pos) {
+ onHeaderContextMenu(view, pos);
+ });
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/tests/CMakeLists.txt b/libs/uibase/tests/CMakeLists.txt
new file mode 100644
index 0000000..4483ff0
--- /dev/null
+++ b/libs/uibase/tests/CMakeLists.txt
@@ -0,0 +1,13 @@
+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 NO_MAIN NO_MOCK WARNINGS 4 AUTOMOC OFF)
+target_link_libraries(uibase-tests PRIVATE uibase)
diff --git a/libs/uibase/tests/cmake/CMakeLists.txt b/libs/uibase/tests/cmake/CMakeLists.txt
new file mode 100644
index 0000000..e1d636f
--- /dev/null
+++ b/libs/uibase/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/libs/uibase/tests/cmake/plugin.cpp b/libs/uibase/tests/cmake/plugin.cpp
new file mode 100644
index 0000000..8356dcd
--- /dev/null
+++ b/libs/uibase/tests/cmake/plugin.cpp
@@ -0,0 +1,4 @@
+#include <uibase/iplugin.h>
+
+class Plugin : public MOBase::IPlugin
+{};
diff --git a/libs/uibase/tests/test_formatters.cpp b/libs/uibase/tests/test_formatters.cpp
new file mode 100644
index 0000000..4093112
--- /dev/null
+++ b/libs/uibase/tests/test_formatters.cpp
@@ -0,0 +1,48 @@
+#pragma warning(push)
+#pragma warning(disable : 4668)
+#include <gtest/gtest.h>
+#pragma warning(pop)
+
+#include <QString>
+#include <vector>
+
+#include <uibase/formatters.h>
+
+#include <format>
+
+TEST(FormatterTest, Enum)
+{
+ enum class E
+ {
+ a = 1,
+ b = 2
+ };
+ ASSERT_EQ("1", std::format("{}", E::a));
+ ASSERT_EQ("2", std::format("{}", E::b));
+}
+
+// simply test that we can format between string types
+TEST(FormatterTest, String)
+{
+ using namespace std::string_literals;
+ ASSERT_EQ("Hello World!", std::format("{}", L"Hello World!"s));
+ ASSERT_EQ("Hello World!", std::format("{}", QString("Hello World!")));
+ ASSERT_EQ(L"Hello World!", std::format(L"{}", QString("Hello World!")));
+}
+
+TEST(FormatterTest, RandomAccessContainer)
+{
+ // note: this is standard since C++23, so this does not test much, and unfortunately
+ // there is no way to customize the output format of the random access container
+ ASSERT_EQ("[]", std::format("{}", std::vector<int>{}));
+ ASSERT_EQ("[1, 2, 3]", std::format("{}", std::vector{1, 2, 3}));
+ ASSERT_EQ("[1, 2, 3, 4, 5, 6, 7]",
+ std::format("{}", std::vector{1, 2, 3, 4, 5, 6, 7}));
+
+ ASSERT_EQ("[\"AL\", \"Holt59\", \"Silarn\"]",
+ std::format("{}", QStringList{"AL", "Holt59", "Silarn"}));
+
+ ASSERT_EQ("[QVariant(type=QString, value=MO2), QVariant(type=bool, value=true), "
+ "QVariant(type=int, value=45)]",
+ std::format("{}", QVariantList{"MO2", true, 45}));
+}
diff --git a/libs/uibase/tests/test_ifiletree.cpp b/libs/uibase/tests/test_ifiletree.cpp
new file mode 100644
index 0000000..74fe3d7
--- /dev/null
+++ b/libs/uibase/tests/test_ifiletree.cpp
@@ -0,0 +1,1172 @@
+#pragma warning(push)
+#pragma warning(disable : 4668)
+#include <gtest/gtest.h>
+#pragma warning(pop)
+
+#include <algorithm>
+#include <ranges>
+#include <string>
+#include <unordered_set>
+#include <variant>
+
+#include <uibase/ifiletree.h>
+
+std::ostream& operator<<(std::ostream& os, const QString& str)
+{
+ return os << str.toStdString();
+}
+
+using namespace MOBase;
+
+namespace std
+{
+// If you can't declare the function in the class it's important that PrintTo()
+// is defined in the SAME namespace that defines Point. C++'s look-up rules
+// rely on that.
+void PrintTo(std::shared_ptr<const FileTreeEntry> entry, std::ostream* os)
+{
+ *os << entry->pathFrom(nullptr, "/");
+}
+} // namespace std
+
+/**
+ *
+ */
+struct FileListTree : public IFileTree
+{
+ using File = std::pair<QStringList, bool>;
+
+ std::shared_ptr<IFileTree> makeDirectory(std::shared_ptr<const IFileTree> parent,
+ QString name,
+ std::vector<File>&& files) const
+ {
+ return std::shared_ptr<FileListTree>(
+ new FileListTree(parent, name, std::move(files)));
+ }
+
+ std::shared_ptr<IFileTree> makeDirectory(std::shared_ptr<const IFileTree> parent,
+ QString name) const
+ {
+ return std::shared_ptr<FileListTree>(new FileListTree(parent, name));
+ }
+
+ bool populated() const { return m_Populated; }
+
+ virtual bool doPopulate(std::shared_ptr<const IFileTree> parent,
+ std::vector<std::shared_ptr<FileTreeEntry>>& entries) const
+ {
+ // We know that the files are sorted:
+ QString currentName = "";
+ std::vector<File> currentFiles;
+ for (auto& p : m_Files) {
+ if (currentName == "") {
+ currentName = p.first[0];
+ }
+
+ if (currentName != p.first[0]) {
+ entries.push_back(makeDirectory(parent, currentName, std::move(currentFiles)));
+ currentFiles.clear();
+ }
+
+ currentName = p.first[0];
+
+ if (p.first.size() == 1) {
+ if (!p.second) {
+ entries.push_back(makeFile(parent, currentName));
+ currentName = "";
+ }
+ } else {
+ currentFiles.push_back(
+ {QStringList(p.first.begin() + 1, p.first.end()), p.second});
+ }
+ }
+
+ if (currentName != "") {
+ entries.push_back(makeDirectory(parent, currentName, std::move(currentFiles)));
+ }
+
+ m_Populated = true;
+
+ return false;
+ }
+
+ virtual std::shared_ptr<IFileTree> doClone() const override
+ {
+ return std::shared_ptr<FileListTree>(new FileListTree(nullptr, name(), m_Files));
+ }
+
+public:
+ static std::shared_ptr<IFileTree>
+ makeTree(std::vector<std::pair<QString, bool>>&& files)
+ {
+ std::sort(std::begin(files), std::end(files),
+ [](const std::pair<QString, bool>& a, const std::pair<QString, bool>& b) {
+ return FileNameComparator::compare(a.first, b.first) < 0;
+ });
+
+ std::vector<File> pFiles;
+
+ for (auto p : files) {
+ pFiles.push_back({p.first.split("/", Qt::SkipEmptyParts), p.second});
+ }
+
+ return std::shared_ptr<FileListTree>(
+ new FileListTree(nullptr, "", std::move(pFiles)));
+ }
+
+protected:
+ FileListTree(std::shared_ptr<const IFileTree> parent, QString name)
+ : FileTreeEntry(parent, name), IFileTree()
+ {}
+ FileListTree(std::shared_ptr<const IFileTree> parent, QString name,
+ std::vector<File> const& files)
+ : FileTreeEntry(parent, name), IFileTree(), m_Files(files)
+ {}
+ FileListTree(std::shared_ptr<const IFileTree> parent, QString name,
+ std::vector<File>&& files)
+ : FileTreeEntry(parent, name), IFileTree(), m_Files(std::move(files))
+ {}
+
+ mutable bool m_Populated = false;
+ std::vector<File> m_Files;
+};
+
+/**
+ * @brief Check if the given tree has been populated.
+ *
+ * Since IFileTree does not expose the "populated" flag, this is a convenient
+ * method that simply downcast to `FileListTree` and check `populated()` on it.
+ *
+ * @param tree The tree to check.
+ *
+ * @return true if the tree has been populated, false otherwize.
+ */
+bool populated(std::shared_ptr<const IFileTree> tree)
+{
+ return std::dynamic_pointer_cast<const FileListTree>(tree)->populated();
+}
+
+/**
+ * @brief Retrieve all the entry in the given tree.
+ *
+ * @param fileTree The tree to get the entries from.
+ *
+ * @return a vector containing all the entries in the tree.
+ */
+std::vector<std::shared_ptr<const FileTreeEntry>>
+getAllEntries(std::shared_ptr<const IFileTree> fileTree)
+{
+ std::vector<std::shared_ptr<const FileTreeEntry>> entries;
+ for (auto entry : *fileTree) {
+ entries.push_back(entry);
+ if (entry->isDir()) {
+ auto childEntries = getAllEntries(entry->astree());
+ entries.insert(entries.end(), childEntries.begin(), childEntries.end());
+ }
+ }
+ return entries;
+}
+
+/**
+ * @brief Check that the given file tree match the given entries.
+ *
+ * This is probably pretty slow but it is only for unit testing. This will check
+ * both way: all entries in the vector must be in the tree at the right place, and
+ * all entries in the tree must be in the vector.
+ *
+ * @param fileTree The tree to check.
+ * @param entries The entries to check. Filenames must be separated by /. Must contain
+ * all the entry, including intermediate directories, except the root.
+ *
+ */
+void assertTreeEquals(std::shared_ptr<const IFileTree> fileTree,
+ std::vector<std::pair<QString, bool>> const& entries)
+{
+ // Check that all entries are in the tree:
+ for (auto& entry : entries) {
+ auto treeEntry = fileTree->find(entry.first);
+ ASSERT_NE(treeEntry, nullptr)
+ << "Entry " << entry.first << " not found in the tree.";
+ ASSERT_EQ(entry.second, treeEntry->isDir())
+ << "Entry " << entry.first << " is not of the right type.";
+ }
+
+ // Check that all entries in the tree are in the vector:
+ auto treeEntries = getAllEntries(fileTree);
+ for (auto& entry : treeEntries) {
+ auto path = entry->pathFrom(fileTree, "/");
+ auto it = std::find_if(entries.begin(), entries.end(), [&path](auto const& p) {
+ return p.first.compare(path, Qt::CaseInsensitive) == 0;
+ });
+ ASSERT_NE(it, entries.end()) << "Entry '" << path << "' not expected in the tree.";
+ ASSERT_EQ(it->second, entry->isDir())
+ << "Entry '" << path << "' is not of the right type.";
+ }
+}
+
+/**
+ * @brief Create a mapping from path to file entry for the given tree.
+ *
+ * @param fileTree The tree to create the mapping from.
+ *
+ * @return a mapping from path (separated by /) to file entry.
+ */
+std::map<QString, std::shared_ptr<const FileTreeEntry>>
+createMapping(std::shared_ptr<const IFileTree> fileTree)
+{
+ std::map<QString, std::shared_ptr<const FileTreeEntry>> mapping;
+ for (auto entry : *fileTree) {
+ mapping[entry->path("/")] = entry;
+ if (entry->isDir()) {
+ auto tmp = createMapping(entry->astree());
+ mapping.insert(std::begin(tmp), std::end(tmp));
+ }
+ }
+ return mapping;
+}
+
+TEST(IFileTreeTest, ExtensionComputedCorrectly)
+{
+ // Fake tree to create entry:
+ std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree({});
+
+ auto a = fileTree->addFile("a.txt");
+ EXPECT_EQ(a->name(), "a.txt");
+ EXPECT_EQ(a->suffix(), "txt");
+
+ fileTree->move(a, "a.c.b");
+ EXPECT_EQ(a->name(), "a.c.b");
+ EXPECT_EQ(a->suffix(), "b");
+}
+
+TEST(IFileTreeTest, TreeIsPopulatedCorrectly)
+{
+ std::vector<std::pair<QString, bool>> strTree{{"a/", true}, {"b", true},
+ {"c.x", false}, {"d.y", false},
+ {"e/q/c.t", false}, {"e/q/p", true}};
+
+ std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree(std::move(strTree));
+
+ ASSERT_NE(fileTree, nullptr);
+
+ ASSERT_TRUE(fileTree->exists("a"));
+ ASSERT_TRUE(fileTree->exists("b"));
+ ASSERT_TRUE(fileTree->exists("c.x"));
+ ASSERT_TRUE(fileTree->exists("d.y"));
+ ASSERT_TRUE(fileTree->exists("e"));
+ ASSERT_TRUE(fileTree->exists("e/q"));
+ ASSERT_TRUE(fileTree->exists("e/q/c.t"));
+ ASSERT_TRUE(fileTree->exists("e/q/p"));
+
+ assertTreeEquals(fileTree, {{"a", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e", true},
+ {"e/q", true},
+ {"e/q/c.t", false},
+ {"e/q/p", true}});
+
+ // Retrieve the entry:
+ {
+ std::shared_ptr<FileTreeEntry> a = fileTree->find("a"), b = fileTree->find("b"),
+ cx = fileTree->find("c.x"),
+ dy = fileTree->find("d.y"), e = fileTree->find("e"),
+ e_q = fileTree->find("e/q"),
+ e_q_ct = fileTree->find("e/q/c.t"),
+ e_q_p = fileTree->find("e/q/p");
+
+ EXPECT_NE(a, nullptr);
+ EXPECT_TRUE(a->isDir());
+ EXPECT_EQ(a->astree(), a);
+ EXPECT_EQ(a->name(), "a");
+ EXPECT_EQ(a->path("/"), "a");
+ EXPECT_NE(b, nullptr);
+ EXPECT_TRUE(b->isDir());
+ EXPECT_EQ(b->astree(), b);
+ EXPECT_EQ(b->name(), "b");
+ EXPECT_EQ(b->path("/"), "b");
+ EXPECT_NE(cx, nullptr);
+ EXPECT_TRUE(cx->isFile());
+ EXPECT_EQ(cx->astree(), nullptr);
+ EXPECT_EQ(cx->name(), "c.x");
+ EXPECT_EQ(cx->path("/"), "c.x");
+ EXPECT_NE(dy, nullptr);
+ EXPECT_TRUE(dy->isFile());
+ EXPECT_EQ(dy->astree(), nullptr);
+ EXPECT_EQ(dy->name(), "d.y");
+ EXPECT_EQ(dy->path("/"), "d.y");
+ EXPECT_NE(e, nullptr);
+ EXPECT_TRUE(e->isDir());
+ EXPECT_EQ(e->astree(), e);
+ EXPECT_EQ(e->name(), "e");
+ EXPECT_EQ(e->path("/"), "e");
+ EXPECT_NE(e_q, nullptr);
+ EXPECT_TRUE(e_q->isDir());
+ EXPECT_EQ(e_q->astree(), e_q);
+ EXPECT_EQ(e_q->name(), "q");
+ EXPECT_EQ(e_q->path("/"), "e/q");
+ EXPECT_NE(e_q_ct, nullptr);
+ EXPECT_TRUE(e_q_ct->isFile());
+ EXPECT_EQ(e_q_ct->astree(), nullptr);
+ EXPECT_EQ(e_q_ct->name(), "c.t");
+ EXPECT_EQ(e_q_ct->path("/"), "e/q/c.t");
+ EXPECT_NE(e_q_p, nullptr);
+ EXPECT_TRUE(e_q_p->isDir());
+ EXPECT_EQ(e_q_p->astree(), e_q_p);
+ EXPECT_EQ(e_q_p->name(), "p");
+ EXPECT_EQ(e_q_p->path("/"), "e/q/p");
+
+ // Some relation check:
+ EXPECT_EQ(a->parent(), fileTree);
+ EXPECT_EQ(b->parent(), fileTree);
+ EXPECT_EQ(cx->parent(), fileTree);
+ EXPECT_EQ(dy->parent(), fileTree);
+ EXPECT_EQ(e->parent(), fileTree);
+ EXPECT_EQ(e_q->parent(), e->astree());
+ EXPECT_EQ(e_q_ct->parent(), e_q->astree());
+ EXPECT_EQ(e_q_p->parent(), e_q->astree());
+
+ // Check that we can reach the children:
+ EXPECT_EQ(e->astree()->find("q"), e_q);
+ EXPECT_EQ(e->astree()->find("q/c.t"), e_q_ct);
+ EXPECT_EQ(e->astree()->find("q/p"), e_q_p);
+
+ // Check the content:
+ EXPECT_EQ(a->astree()->size(), std::size_t{0});
+ EXPECT_TRUE(a->astree()->empty());
+ EXPECT_EQ(a->astree()->begin(), a->astree()->end());
+ EXPECT_EQ(b->astree()->size(), std::size_t{0});
+ EXPECT_TRUE(b->astree()->empty());
+ EXPECT_EQ(b->astree()->begin(), b->astree()->end());
+ EXPECT_EQ(cx->astree(), nullptr);
+ EXPECT_EQ(dy->astree(), nullptr);
+ EXPECT_EQ(e->astree()->size(), std::size_t{1});
+ EXPECT_EQ(e->astree()->at(0), e_q);
+ EXPECT_EQ(e_q->astree()->size(), std::size_t{2});
+ EXPECT_NE(std::find(e_q->astree()->begin(), e_q->astree()->end(), e_q_ct),
+ e_q->astree()->end());
+ EXPECT_NE(std::find(e_q->astree()->begin(), e_q->astree()->end(), e_q_p),
+ e_q->astree()->end());
+
+ EXPECT_EQ(a->pathFrom(fileTree), "a");
+ EXPECT_EQ(a->path(), "a");
+ EXPECT_EQ(b->pathFrom(fileTree), "b");
+ EXPECT_EQ(b->path(), "b");
+ EXPECT_EQ(cx->path(), "c.x");
+ EXPECT_EQ(dy->path(), "d.y");
+ EXPECT_EQ(e->path(), "e");
+ EXPECT_EQ(e_q->path(), "e\\q");
+ EXPECT_EQ(e_q->pathFrom(e->astree()), "q");
+ EXPECT_EQ(e_q_ct->path("/"), "e/q/c.t");
+ EXPECT_EQ(e_q_ct->pathFrom(e->astree()), "q\\c.t");
+ EXPECT_EQ(e_q_ct->pathFrom(e_q->astree(), "/"), "c.t");
+ EXPECT_EQ(e_q_p->path(), "e\\q\\p");
+ EXPECT_EQ(e_q_p->path("/"), "e/q/p");
+ EXPECT_EQ(e_q_p->pathFrom(e->astree()), "q\\p");
+ EXPECT_EQ(e_q_p->pathFrom(e_q->astree()), "p");
+
+ EXPECT_EQ(a->pathFrom(b->astree()), "");
+ EXPECT_EQ(b->pathFrom(a->astree()), "");
+ EXPECT_EQ(e->pathFrom(e_q->astree()), "");
+ }
+
+ {
+ std::shared_ptr<FileTreeEntry> a = fileTree->find("a", FileTreeEntry::DIRECTORY),
+ b = fileTree->find("b", FileTreeEntry::DIRECTORY),
+ cx = fileTree->find("c.x", FileTreeEntry::FILE),
+ dy = fileTree->find("d.y", FileTreeEntry::FILE),
+ e = fileTree->find("e", FileTreeEntry::DIRECTORY),
+ e_q =
+ fileTree->find("e/q", FileTreeEntry::DIRECTORY),
+ e_q_ct =
+ fileTree->find("e/q/c.t", FileTreeEntry::FILE),
+ e_q_p = fileTree->find("e/q/p",
+ FileTreeEntry::DIRECTORY);
+
+ EXPECT_TRUE((a != nullptr && a->isDir() && a->name() == "a"));
+ EXPECT_TRUE((b != nullptr && b->isDir() && b->name() == "b"));
+ EXPECT_TRUE((cx != nullptr && cx->isFile() && cx->name() == "c.x"));
+ EXPECT_TRUE((dy != nullptr && dy->isFile() && dy->name() == "d.y"));
+ EXPECT_TRUE((e != nullptr && e->isDir() && e->name() == "e"));
+ EXPECT_TRUE((e_q != nullptr && e_q->isDir() && e_q->name() == "q"));
+ EXPECT_TRUE((e_q_ct != nullptr && e_q_ct->isFile() && e_q_ct->name() == "c.t"));
+ EXPECT_TRUE((e_q_p != nullptr && e_q_p->isDir() && e_q_p->name() == "p"));
+
+ EXPECT_EQ(fileTree->find("a", FileTreeEntry::FILE), nullptr);
+ EXPECT_EQ(fileTree->find("b", FileTreeEntry::FILE), nullptr);
+ EXPECT_EQ(fileTree->find("c.x", FileTreeEntry::DIRECTORY), nullptr);
+ EXPECT_EQ(fileTree->find("d.y", FileTreeEntry::DIRECTORY), nullptr);
+ EXPECT_EQ(fileTree->find("e", FileTreeEntry::FILE), nullptr);
+ EXPECT_EQ(fileTree->find("e/q", FileTreeEntry::FILE), nullptr);
+ EXPECT_EQ(fileTree->find("e/q/c.t", FileTreeEntry::DIRECTORY), nullptr);
+ EXPECT_EQ(fileTree->find("e/q/p", FileTreeEntry::FILE), nullptr);
+ }
+}
+
+TEST(IFileTreeTest, TreeIsDestructedCorrectly)
+{
+ std::vector<std::pair<QString, bool>> strTree{{"a/", true}, {"b", true},
+ {"c.x", false}, {"d.y", false},
+ {"e/q/c.t", false}, {"e/q/p", true}};
+
+ std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree(std::move(strTree));
+
+ EXPECT_NE(fileTree, nullptr);
+
+ // Retrieve weak ptr for the entry:
+ std::weak_ptr<FileTreeEntry> a = fileTree->find("a"), b = fileTree->find("b"),
+ cx = fileTree->find("c.x"), dy = fileTree->find("d.y"),
+ e = fileTree->find("e"), e_q = fileTree->find("e/q"),
+ e_q_ct = fileTree->find("e/q/c.t"),
+ e_q_p = fileTree->find("e/q/p");
+
+ // And for the trees:
+ std::weak_ptr<IFileTree> r_t = fileTree, a_t = a.lock()->astree(),
+ b_t = b.lock()->astree(), e_t = e.lock()->astree(),
+ e_q_t = e_q.lock()->astree(),
+ e_q_p_t = e_q_p.lock()->astree();
+
+ // Release the base tree:
+ fileTree.reset();
+
+ EXPECT_TRUE(a.expired());
+ EXPECT_TRUE(b.expired());
+ EXPECT_TRUE(cx.expired());
+ EXPECT_TRUE(dy.expired());
+ EXPECT_TRUE(e.expired());
+ EXPECT_TRUE(e_q.expired());
+ EXPECT_TRUE(e_q_ct.expired());
+ EXPECT_TRUE(e_q_p.expired());
+
+ EXPECT_TRUE(a_t.expired());
+ EXPECT_TRUE(b_t.expired());
+ EXPECT_TRUE(e_t.expired());
+ EXPECT_TRUE(e_q_t.expired());
+ EXPECT_TRUE(e_q_p_t.expired());
+}
+
+TEST(IFileTreeTest, BasicTreeManipulation)
+{
+ std::vector<std::pair<QString, bool>> strTree{{"a/", true}, {"b", true},
+ {"c.x", false}, {"d.y", false},
+ {"e/q/c.t", false}, {"e/q/p", true}};
+
+ std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree(std::move(strTree));
+
+ EXPECT_NE(fileTree, nullptr);
+
+ // Retrieve the entry:
+ std::shared_ptr<FileTreeEntry> a = fileTree->find("a"), b = fileTree->find("b"),
+ cx = fileTree->find("c.x"), dy = fileTree->find("d.y"),
+ e = fileTree->find("e"), e_q = fileTree->find("e/q"),
+ e_q_ct = fileTree->find("e/q/c.t"),
+ e_q_p = fileTree->find("e/q/p");
+
+ EXPECT_TRUE(b->moveTo(a->astree()));
+ EXPECT_FALSE(fileTree->exists("b"));
+ EXPECT_EQ(fileTree->find("a/b"), b);
+ EXPECT_TRUE(a->astree()->exists("b"));
+ EXPECT_EQ(a->astree()->find("b"), b);
+ EXPECT_EQ(a->astree()->size(), std::size_t{1});
+ EXPECT_EQ(a->astree()->at(0), b);
+}
+
+TEST(IFileTreeTest, IterOperations)
+{
+ auto tree =
+ FileListTree::makeTree({{"a", true}, {"c", true}, {"b", false}, {"d", false}});
+
+ // Order should be a -> c -> b -> d
+ std::vector expected{tree->find("a"), tree->find("c"), tree->find("b"),
+ tree->find("d")};
+ std::vector entries(std::begin(*tree), std::end(*tree));
+ EXPECT_EQ(entries, expected);
+
+ // Order should be reversed:
+ expected = std::vector(expected.rbegin(), expected.rend());
+ entries = std::vector(std::rbegin(*tree), std::rend(*tree));
+ EXPECT_EQ(entries, expected);
+
+ // We can erasae in the middle:
+ for (auto it = tree->begin(); it != tree->end();) {
+ if ((*it)->name() == "b") {
+ it = tree->erase(*it);
+ // Check that the returned iterator is valid (it should be the iterator
+ // to d):
+ EXPECT_EQ(it, tree->end() - 1);
+ EXPECT_EQ(*it, tree->find("d"));
+ } else {
+ ++it;
+ }
+ }
+ assertTreeEquals(tree, {{"a", true}, {"c", true}, {"d", false}});
+}
+
+TEST(IFileTreeTest, AddOperations)
+{
+ {
+ auto fileTree = FileListTree::makeTree(
+ {{"a", true}, {"c.x", false}, {"e/q/c.t", false}, {"e/q/p", true}});
+ auto map = createMapping(fileTree);
+
+ EXPECT_EQ(fileTree->addFile("a"), nullptr);
+ EXPECT_EQ(fileTree->addFile("c.x"), nullptr);
+ EXPECT_EQ(fileTree->addFile("e"), nullptr);
+ EXPECT_EQ(fileTree->addFile("e/q"), nullptr);
+ EXPECT_EQ(fileTree->addFile("e/q/c.t"), nullptr);
+ EXPECT_EQ(fileTree->addFile("e/q/p"), nullptr);
+
+ auto a_p = fileTree->addFile("a/p");
+ EXPECT_NE(a_p, nullptr);
+ EXPECT_EQ(a_p->parent(), map["a"]);
+
+ auto e_q_ct = fileTree->addFile("e/q/c.t", true);
+ EXPECT_NE(e_q_ct, nullptr);
+ EXPECT_EQ(e_q_ct->parent(), map["e/q"]);
+ EXPECT_EQ(map["e/q/c.t"]->parent(), nullptr);
+ EXPECT_EQ(map["e/q"]->astree()->size(), std::size_t{2});
+
+ // Directory are replaced with addFile():
+ auto e_q = fileTree->addFile("e/q", true);
+ EXPECT_NE(e_q, nullptr);
+ EXPECT_EQ(e_q->parent(), map["e"]);
+ EXPECT_EQ(map["e/q"]->parent(), nullptr);
+ EXPECT_EQ(map["e"]->astree()->size(), std::size_t{1});
+ }
+}
+
+TEST(IFileTreeTest, TreeInsertOperations)
+{
+
+ // Test failure:
+ {
+ auto fileTree = FileListTree::makeTree({{"a/", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e/q/c.t", false},
+ {"e/q/p", true},
+ {"e/q/z/", true},
+ {"e/q/z/a.t", false},
+ {"e/q/z/b", true},
+ {"f/q/c.t", false},
+ {"f/q/o", true},
+ {"f/q/z/b", false},
+ {"f/q/z/c.t", false}});
+
+ EXPECT_NE(fileTree, nullptr);
+
+ // Retrieve the entry:
+ auto map = createMapping(fileTree);
+ auto e = fileTree->findDirectory("e");
+ auto f_q = fileTree->findDirectory("f/q");
+
+ auto it = e->insert(f_q, IFileTree::InsertPolicy::FAIL_IF_EXISTS);
+ EXPECT_EQ(it, e->end());
+ EXPECT_EQ(f_q->parent(), fileTree->find("f"));
+ }
+
+ // Test replace:
+ {
+ auto fileTree = FileListTree::makeTree({{"a/", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e/q/c.t", false},
+ {"e/q/p", true},
+ {"e/q/z/", true},
+ {"e/q/z/a.t", false},
+ {"e/q/z/b", true},
+ {"f/q/c.t", false},
+ {"f/q/o", true},
+ {"f/q/z/b", false},
+ {"f/q/z/c.t", false}});
+
+ EXPECT_NE(fileTree, nullptr);
+
+ // Retrieve the entry:
+ auto map = createMapping(fileTree);
+ auto e = fileTree->findDirectory("e");
+ auto f_q = fileTree->findDirectory("f/q");
+
+ auto it = e->insert(f_q, IFileTree::InsertPolicy::REPLACE);
+ EXPECT_NE(it, e->end());
+ EXPECT_EQ(f_q->parent(), e);
+ EXPECT_EQ(map["e/q"]->parent(), nullptr);
+ EXPECT_EQ(e->find("q"), map["f/q"]);
+ EXPECT_TRUE(fileTree->findDirectory("f")->empty());
+ EXPECT_EQ(e->find("q/c.t"), map["f/q/c.t"]);
+ EXPECT_EQ(e->find("q/o"), map["f/q/o"]);
+ EXPECT_EQ(e->find("q/z"), map["f/q/z"]);
+ EXPECT_EQ(e->find("q/z/b"), map["f/q/z/b"]);
+ EXPECT_EQ(e->find("q/z/c.t"), map["f/q/z/c.t"]);
+ }
+
+ // Test merge:
+ {
+ auto fileTree = FileListTree::makeTree({{"a/", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e/q/c.t", false},
+ {"e/q/p", true},
+ {"e/q/z", true},
+ {"e/q/z/a.t", false},
+ {"e/q/z/b", true},
+ {"f/q/c.t", false},
+ {"f/q/o", true},
+ {"f/q/z", true},
+ {"f/q/z/b", false},
+ {"f/q/z/c.t", false}});
+
+ EXPECT_NE(fileTree, nullptr);
+
+ // Retrieve the entry:
+ auto map = createMapping(fileTree);
+ auto e = fileTree->findDirectory("e");
+ auto f_q = fileTree->findDirectory("f/q");
+
+ auto it = e->insert(f_q, IFileTree::InsertPolicy::MERGE);
+ assertTreeEquals(e, {{"q", true},
+ {"q/o", true},
+ {"q/p", true},
+ {"q/z", true},
+ {"q/c.t", false},
+ {"q/z/a.t", false},
+ {"q/z/c.t", false},
+ {"q/z/b", false}});
+ EXPECT_EQ(e->find("q/z/b"), map["f/q/z/b"]);
+ EXPECT_EQ(fileTree->findDirectory("f")->size(), std::size_t{0});
+ EXPECT_EQ(map["f/q"]->parent(), nullptr);
+ EXPECT_EQ(map["f/q/z"]->parent(), nullptr);
+ }
+}
+
+TEST(IFileTreeTest, TreeMoveAndCopyOperations)
+{
+ {
+ auto tree1 = FileListTree::makeTree(
+ {{"a/b/m.y", false}, {"a/b/c", true}, {"b/", true}, {"c", false}});
+ auto a = tree1->findDirectory("a");
+ EXPECT_FALSE(populated(a));
+
+ tree1->move(tree1->find("a"), "a1");
+
+ // Moving the tree should not have populated it:
+ EXPECT_EQ(tree1->find("a"), nullptr);
+ EXPECT_EQ(tree1->find("a1"), a);
+ EXPECT_FALSE(populated(a));
+
+ tree1->copy(tree1->find("a1"), "a2");
+
+ // Copying the tree should not have populated it:
+ EXPECT_FALSE(populated(a));
+ EXPECT_FALSE(populated(tree1->findDirectory("a2")));
+ EXPECT_EQ(tree1->find("a1"), a);
+ EXPECT_NE(tree1->find("a1"), tree1->find("a2"));
+
+ assertTreeEquals(tree1, {
+ {"a1", true},
+ {"a1/b", true},
+ {"a1/b/c", true},
+ {"a1/b/m.y", false},
+ {"a2", true},
+ {"a2/b", true},
+ {"a2/b/c", true},
+ {"a2/b/m.y", false},
+ {"b", true},
+ {"c", false},
+ });
+
+ // Everything should be populated now:
+ EXPECT_TRUE(populated(tree1->findDirectory("a1")));
+ EXPECT_TRUE(populated(tree1->findDirectory("a2")));
+
+ QString a1("a1/"), a2("a2/");
+ for (auto p : {"b", "b/c", "b/m.y"}) {
+ EXPECT_NE(tree1->find(a1 + p), tree1->find(a2 + p))
+ << "Entry '" << (a1 + p) << "' and '" << (a2 + p) << "' should be different.";
+ }
+ }
+}
+
+TEST(IFileTreeTest, TreeMergeOperations)
+{
+
+ {
+ auto fileTree = FileListTree::makeTree({{"a/", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e/q/c.t", false},
+ {"e/q/p", true}});
+
+ EXPECT_NE(fileTree, nullptr);
+
+ // Retrieve the entry:
+ auto map = createMapping(fileTree);
+ auto e = fileTree->findDirectory("e");
+ auto e_q = fileTree->findDirectory("e/q");
+
+ // Merge e in the root:
+ IFileTree::OverwritesType overwrites;
+ auto noverwrites = fileTree->merge(e, &overwrites);
+
+ EXPECT_EQ(noverwrites, std::size_t{0});
+ EXPECT_TRUE(overwrites.empty());
+ EXPECT_EQ(e->size(), std::size_t{0});
+ assertTreeEquals(fileTree, {{"a", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e", true},
+ {"q", true},
+ {"q/c.t", false},
+ {"q/p", true}});
+
+ auto p = fileTree->addFile("p");
+ EXPECT_NE(p, nullptr);
+
+ // Not: e/q is not q
+ overwrites.clear();
+ noverwrites = fileTree->merge(e_q, &overwrites);
+ EXPECT_EQ(noverwrites, std::size_t{1});
+ EXPECT_EQ(overwrites.size(), std::size_t{1});
+ EXPECT_EQ(overwrites[p], map["e/q/p"]);
+ assertTreeEquals(fileTree, {{"a", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e", true},
+ {"q", true},
+ {"c.t", false},
+ {"p", true}});
+ // Note: the "p" at the root should be the one under q initially.
+ EXPECT_EQ(fileTree->find("p"), map["e/q/p"]);
+ }
+
+ // Merge failure:
+ {
+ auto tree1 = FileListTree::makeTree({{"a/", true},
+ {"b", true},
+ {"c.x", false},
+ {"d.y", false},
+ {"e/q/c.t", false},
+ {"e/q/p", true}});
+
+ std::size_t noverwrites = tree1->findDirectory("e")->merge(tree1);
+ EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED);
+
+ noverwrites = tree1->findDirectory("e/q")->merge(tree1);
+ EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED);
+
+ noverwrites = tree1->merge(tree1);
+ EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED);
+ }
+
+ //
+ {
+ auto tree1 = FileListTree::makeTree({{"a/b/c/m.y", false},
+ {"a/b/c/n", true},
+ {"a/b/x.t", false},
+ {"a/b/y.t", false},
+ {"b/", true},
+ {"c", false}});
+ auto map1 = createMapping(tree1);
+
+ auto tree2 = FileListTree::makeTree({{"a/b/c/m.y", false},
+ {"a/b/c/n", false}, // n is a file here
+ {"a/b/y.t", false},
+ {"b/v", false},
+ {"b/e", true}});
+ auto map2 = createMapping(tree2);
+
+ IFileTree::OverwritesType overwrites;
+ std::size_t noverwrites = tree1->merge(tree2, &overwrites);
+
+ EXPECT_EQ(noverwrites, std::size_t{3});
+ EXPECT_EQ(noverwrites, overwrites.size());
+ EXPECT_EQ(overwrites[map1["a/b/c/m.y"]], map2["a/b/c/m.y"]);
+ EXPECT_EQ(overwrites[map1["a/b/c/n"]], map2["a/b/c/n"]);
+ EXPECT_EQ(overwrites[map1["a/b/y.t"]], map2["a/b/y.t"]);
+
+ assertTreeEquals(tree1, {{"a", true},
+ {"b", true},
+ {"c", false},
+ {"a/b", true},
+ {"a/b/c", true},
+ {"a/b/c/m.y", false},
+ {"a/b/c/n", false},
+ {"a/b/x.t", false},
+ {"a/b/y.t", false},
+ {"b/v", false},
+ {"b/e", true}});
+
+ // Merged directories should be the one from the original tree:
+ EXPECT_EQ(tree1->find("a"), map1["a"]);
+ EXPECT_EQ(tree1->find("a/b"), map1["a/b"]);
+ EXPECT_EQ(tree1->find("a/b/c"), map1["a/b/c"]);
+ EXPECT_EQ(tree1->find("b"), map1["b"]);
+
+ // Overriden:
+ EXPECT_EQ(tree1->find("a/b/c/m.y"), map2["a/b/c/m.y"]);
+ EXPECT_EQ(tree1->find("a/b/c/n"), map2["a/b/c/n"]);
+ EXPECT_EQ(tree1->find("a/b/y.t"), map2["a/b/y.t"]);
+ }
+}
+
+TEST(IFileTreeTest, TreeWalkOperations)
+{
+
+ auto fileTree = FileListTree::makeTree({{"a/", true},
+ {"b", true},
+ {"b/u", false},
+ {"b/v", false},
+ {"c.x", false},
+ {"d.y", false},
+ {"e/q/c.t", false},
+ {"e/q/p", true}});
+
+ auto map = createMapping(fileTree);
+
+ // Note: Testing specific order here, while in reality user should not rely
+ // on it (and it is not specified, on purpose). Only guarantee is that a folder
+ // is visited before its children.
+ {
+ // Populate the vector:
+ std::vector<std::pair<QString, std::shared_ptr<const FileTreeEntry>>> entries;
+ fileTree->walk(
+ [&entries](auto path, auto entry) {
+ entries.push_back({path, entry});
+ return IFileTree::WalkReturn::CONTINUE;
+ },
+ "/");
+
+ decltype(entries) expected{{"", map["a"]}, {"", map["b"]},
+ {"b/", map["b/u"]}, {"b/", map["b/v"]},
+ {"", map["e"]}, {"e/", map["e/q"]},
+ {"e/q/", map["e/q/p"]}, {"e/q/", map["e/q/c.t"]},
+ {"", map["c.x"]}, {"", map["d.y"]}};
+ EXPECT_EQ(entries, expected);
+
+ entries.clear();
+ fileTree->walk(
+ [&entries](auto path, auto entry) {
+ if (entry->name() == "e") {
+ return IFileTree::WalkReturn::STOP;
+ }
+ entries.push_back({path, entry});
+ return IFileTree::WalkReturn::CONTINUE;
+ },
+ "/");
+
+ // Note: This assumes a given order, while in reality it is not specified.
+ expected = {
+ {"", map["a"]},
+ {"", map["b"]},
+ {"b/", map["b/u"]},
+ {"b/", map["b/v"]},
+ };
+ EXPECT_EQ(entries, expected);
+
+ entries.clear();
+ fileTree->walk(
+ [&entries](auto path, auto entry) {
+ if (entry->name() == "e") {
+ return IFileTree::WalkReturn::SKIP;
+ }
+ entries.push_back({path, entry});
+ return IFileTree::WalkReturn::CONTINUE;
+ },
+ "/");
+
+ // Note: This assumes a given order, while in reality it is not specified.
+ expected = {{"", map["a"]}, {"", map["b"]}, {"b/", map["b/u"]},
+ {"b/", map["b/v"]}, {"", map["c.x"]}, {"", map["d.y"]}};
+ EXPECT_EQ(entries, expected);
+ }
+
+ // same as above but with generator version
+ {
+ // Populate the vector:
+ auto entries = walk(fileTree) | std::ranges::to<std::vector>();
+ decltype(entries) expected{map["a"], map["b"], map["b/u"], map["b/v"],
+ map["e"], map["e/q"], map["e/q/p"], map["e/q/c.t"],
+ map["c.x"], map["d.y"]};
+ EXPECT_EQ(entries, expected);
+
+ entries.clear();
+ for (const auto entry : walk(fileTree)) {
+ if (entry->name() == "e") {
+ break; // Stop on e
+ }
+ entries.push_back(entry);
+ }
+
+ // Note: This assumes a given order, while in reality it is not specified.
+ expected = {
+ map["a"],
+ map["b"],
+ map["b/u"],
+ map["b/v"],
+ };
+ EXPECT_EQ(entries, expected);
+
+ // note: third test with SKIP is not possible with generator version
+ }
+}
+
+TEST(IFileTreeTest, TreeGlobOperations)
+{
+ using entrySet = std::unordered_set<std::shared_ptr<const FileTreeEntry>>;
+
+ const auto REGEX = GlobPatternType::REGEX;
+
+ {
+ auto fileTree = FileListTree::makeTree({{"a/", true},
+ {"a/g.t", false},
+ {"b", true},
+ {"b/u", false},
+ {"b/v", false},
+ {"c.x", false},
+ {"d.y", false},
+ {"e/q/c.t", false},
+ {"e/q/m.x", false},
+ {"e/q/p", true}});
+
+ auto map = createMapping(fileTree);
+
+ entrySet entries, expected;
+
+ entries = glob(fileTree, "*") | std::ranges::to<std::unordered_set>();
+ expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, ".*", REGEX) | std::ranges::to<std::unordered_set>();
+ expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**") | std::ranges::to<std::unordered_set>();
+ expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**", REGEX) | std::ranges::to<std::unordered_set>();
+ expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "*.x") | std::ranges::to<std::unordered_set>();
+ expected = {map["c.x"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, ".*[.]x", REGEX) | std::ranges::to<std::unordered_set>();
+ expected = {map["c.x"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**/*.x") | std::ranges::to<std::unordered_set>();
+ expected = {map["c.x"], map["e/q/m.x"]};
+ EXPECT_EQ(entries, expected);
+
+ entries =
+ glob(fileTree, "**/.*[.]x", REGEX) | std::ranges::to<std::unordered_set>();
+ expected = {map["c.x"], map["e/q/m.x"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "*.t") | std::ranges::to<std::unordered_set>();
+ expected = {};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**/*.t") | std::ranges::to<std::unordered_set>();
+ expected = {map["a/g.t"], map["e/q/c.t"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "a/*") | std::ranges::to<std::unordered_set>();
+ expected = {map["a/g.t"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "a/.*", REGEX) | std::ranges::to<std::unordered_set>();
+ expected = {map["a/g.t"]};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**/*.[xt]") | std::ranges::to<std::unordered_set>();
+ expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]};
+ EXPECT_EQ(entries, expected);
+
+ entries =
+ glob(fileTree, "**/.*[.][xt]", REGEX) | std::ranges::to<std::unordered_set>();
+ expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]};
+ EXPECT_EQ(entries, expected);
+ }
+
+ {
+ auto fileTree = FileListTree::makeTree({{"aq.js", false},
+ {"bb", true},
+ {"cm.tx", false},
+ {"dp.js", false},
+ {"ev", false},
+ {"go.ya", false},
+ {"gw.md", false},
+ {"hh", false},
+ {"hl", true},
+ {"in", true},
+ {"mz", true},
+ {"sc", true},
+ {"bb/ce.cp", false},
+ {"bb/cm.tx", false},
+ {"bb/gw", true},
+ {"bb/iw.cp", false},
+ {"bb/js", true},
+ {"bb/px.cp", false},
+ {"hl/ds.in", false},
+ {"in/nu", true},
+ {"mz/tu.js", false},
+ {"sc/cm.tx", false},
+ {"sc/cw.ts", false},
+ {"sc/cz.rc", false},
+ {"sc/dr.cp", false},
+ {"sc/hh.cp", false},
+ {"sc/kn.ui", false},
+ {"sc/lr.cp", false},
+ {"sc/nd.o", false},
+ {"sc/nv.o", false},
+ {"sc/rv.ui", false},
+ {"sc/tv.h", false},
+ {"bb/gw/cp.qm", false},
+ {"bb/gw/hq.qm", false},
+ {"bb/gw/pu.ts", false},
+ {"bb/gw/tu.ts", false},
+ {"bb/js/cm.tx", false},
+ {"bb/js/co.cp", false},
+ {"in/nu/el.h", false},
+ {"in/nu/fj.h", false},
+ {"in/nu/lw", true},
+ {"in/nu/xx", true},
+ {"in/nu/lw/cp.h", false},
+ {"in/nu/lw/go.h", false},
+ {"in/nu/xx/ap.h", false},
+ {"in/nu/xx/qz.h", false}});
+
+ auto map = createMapping(fileTree);
+
+ entrySet entries, expected;
+
+ entries = glob(fileTree, "*.h") | std::ranges::to<std::unordered_set>();
+ expected = {};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "*") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("aq.js"), map.at("bb"), map.at("cm.tx"), map.at("dp.js"),
+ map.at("ev"), map.at("go.ya"), map.at("gw.md"), map.at("hh"),
+ map.at("hl"), map.at("in"), map.at("mz"), map.at("sc")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "*/*") | std::ranges::to<std::unordered_set>();
+ expected = {
+ map.at("bb/ce.cp"), map.at("bb/cm.tx"), map.at("bb/gw"), map.at("bb/iw.cp"),
+ map.at("bb/js"), map.at("bb/px.cp"), map.at("hl/ds.in"), map.at("in/nu"),
+ map.at("mz/tu.js"), map.at("sc/cm.tx"), map.at("sc/cw.ts"), map.at("sc/cz.rc"),
+ map.at("sc/dr.cp"), map.at("sc/hh.cp"), map.at("sc/kn.ui"), map.at("sc/lr.cp"),
+ map.at("sc/nd.o"), map.at("sc/nv.o"), map.at("sc/rv.ui"), map.at("sc/tv.h")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "*/*/*") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("bb/gw/cp.qm"), map.at("bb/gw/hq.qm"), map.at("bb/gw/pu.ts"),
+ map.at("bb/gw/tu.ts"), map.at("bb/js/cm.tx"), map.at("bb/js/co.cp"),
+ map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"),
+ map.at("in/nu/xx")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**") | std::ranges::to<std::unordered_set>();
+ expected = {fileTree, map.at("bb"), map.at("bb/gw"), map.at("bb/js"),
+ map.at("hl"), map.at("in"), map.at("in/nu"), map.at("in/nu/lw"),
+ map.at("in/nu/xx"), map.at("mz"), map.at("sc")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**/*") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("aq.js"),
+ map.at("bb"),
+ map.at("cm.tx"),
+ map.at("dp.js"),
+ map.at("ev"),
+ map.at("go.ya"),
+ map.at("gw.md"),
+ map.at("hh"),
+ map.at("hl"),
+ map.at("in"),
+ map.at("mz"),
+ map.at("sc"),
+ map.at("bb/ce.cp"),
+ map.at("bb/cm.tx"),
+ map.at("bb/gw"),
+ map.at("bb/iw.cp"),
+ map.at("bb/js"),
+ map.at("bb/px.cp"),
+ map.at("bb/gw/cp.qm"),
+ map.at("bb/gw/hq.qm"),
+ map.at("bb/gw/pu.ts"),
+ map.at("bb/gw/tu.ts"),
+ map.at("bb/js/cm.tx"),
+ map.at("bb/js/co.cp"),
+ map.at("hl/ds.in"),
+ map.at("in/nu"),
+ map.at("in/nu/el.h"),
+ map.at("in/nu/fj.h"),
+ map.at("in/nu/lw"),
+ map.at("in/nu/xx"),
+ map.at("in/nu/lw/cp.h"),
+ map.at("in/nu/lw/go.h"),
+ map.at("in/nu/xx/ap.h"),
+ map.at("in/nu/xx/qz.h"),
+ map.at("mz/tu.js"),
+ map.at("sc/cm.tx"),
+ map.at("sc/cw.ts"),
+ map.at("sc/cz.rc"),
+ map.at("sc/dr.cp"),
+ map.at("sc/hh.cp"),
+ map.at("sc/kn.ui"),
+ map.at("sc/lr.cp"),
+ map.at("sc/nd.o"),
+ map.at("sc/nv.o"),
+ map.at("sc/rv.ui"),
+ map.at("sc/tv.h")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**/cm.tx") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("cm.tx"), map.at("bb/cm.tx"), map.at("bb/js/cm.tx"),
+ map.at("sc/cm.tx")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**/sc/**/cm.tx") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("sc/cm.tx")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "**/sc") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("sc")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "in/**") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "in/**/**") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "in/*/*") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"),
+ map.at("in/nu/xx")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "in/*/*.h") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "sc/**/*.cp") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("sc/dr.cp"), map.at("sc/hh.cp"), map.at("sc/lr.cp")};
+ EXPECT_EQ(entries, expected);
+
+ entries = glob(fileTree, "sc/**/n*.o") | std::ranges::to<std::unordered_set>();
+ expected = {map.at("sc/nd.o"), map.at("sc/nv.o")};
+ EXPECT_EQ(entries, expected);
+ }
+}
diff --git a/libs/uibase/tests/test_main.cpp b/libs/uibase/tests/test_main.cpp
new file mode 100644
index 0000000..0399b43
--- /dev/null
+++ b/libs/uibase/tests/test_main.cpp
@@ -0,0 +1,15 @@
+#include <gtest/gtest.h>
+
+#include <QCoreApplication>
+#include <QTranslator>
+
+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/libs/uibase/tests/test_strings.cpp b/libs/uibase/tests/test_strings.cpp
new file mode 100644
index 0000000..0185f83
--- /dev/null
+++ b/libs/uibase/tests/test_strings.cpp
@@ -0,0 +1,47 @@
+#pragma warning(push)
+#pragma warning(disable : 4668)
+#include <gtest/gtest.h>
+#pragma warning(pop)
+
+#include <QCoreApplication>
+
+#include <uibase/strings.h>
+
+#include <format>
+
+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/libs/uibase/tests/test_versioning.cpp b/libs/uibase/tests/test_versioning.cpp
new file mode 100644
index 0000000..08a346f
--- /dev/null
+++ b/libs/uibase/tests/test_versioning.cpp
@@ -0,0 +1,90 @@
+#pragma warning(push)
+#pragma warning(disable : 4668)
+#include <gtest/gtest.h>
+#pragma warning(pop)
+
+#include <QString>
+#include <vector>
+
+#include <uibase/versioning.h>
+
+#include <format>
+
+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/libs/uibase/tests/translations/tests_en.qm b/libs/uibase/tests/translations/tests_en.qm
new file mode 100644
index 0000000..d5ca501
--- /dev/null
+++ b/libs/uibase/tests/translations/tests_en.qm
Binary files differ
diff --git a/libs/uibase/tests/translations/tests_en.ts b/libs/uibase/tests/translations/tests_en.ts
new file mode 100644
index 0000000..1f54d86
--- /dev/null
+++ b/libs/uibase/tests/translations/tests_en.ts
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>uibase-tests</name>
+ <message>
+ <source>Translate to French</source>
+ <translation>Translate to French</translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/uibase/tests/translations/tests_fr.qm b/libs/uibase/tests/translations/tests_fr.qm
new file mode 100644
index 0000000..80958d6
--- /dev/null
+++ b/libs/uibase/tests/translations/tests_fr.qm
Binary files differ
diff --git a/libs/uibase/tests/translations/tests_fr.ts b/libs/uibase/tests/translations/tests_fr.ts
new file mode 100644
index 0000000..eed02a0
--- /dev/null
+++ b/libs/uibase/tests/translations/tests_fr.ts
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fr_FR">
+<context>
+ <name>uibase-tests</name>
+ <message>
+ <source>Translate to French</source>
+ <translation>Traduction en Français</translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/uibase/vcpkg.json b/libs/uibase/vcpkg.json
new file mode 100644
index 0000000..4777918
--- /dev/null
+++ b/libs/uibase/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"]
+ }
+ ]
+ }
+}