diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /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')
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 + +[](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>&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 "(/i)" 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 "(/x)" 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>&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 '%1' 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 "%1" 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 "Reset Dialog Choices" 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 '%1', could not create a temporary file: %2 (error %3)</source> + <oldsource>Failed to save '{}', could not create a temporary file: {} (error {})</oldsource> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="62"/> + <source>removal of "%1" failed: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="71"/> + <source>removal of "%1" failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="75"/> + <source>"%1" doesn'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 "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="682"/> + <location filename="utility.cpp" line="709"/> + <source>failed to copy "%1" to "%2"</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 "mo_interface.log" 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 Binary files differnew file mode 100644 index 0000000..d5ca501 --- /dev/null +++ b/libs/uibase/tests/translations/tests_en.qm 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 Binary files differnew file mode 100644 index 0000000..80958d6 --- /dev/null +++ b/libs/uibase/tests/translations/tests_fr.qm 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"] + } + ] + } +} |
