aboutsummaryrefslogtreecommitdiff
path: root/libs/uibase/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/uibase/src')
-rw-r--r--libs/uibase/src/CMakeLists.txt201
-rw-r--r--libs/uibase/src/delayedfilewriter.cpp49
-rw-r--r--libs/uibase/src/diagnosisreport.cpp25
-rw-r--r--libs/uibase/src/errorcodes.cpp2361
-rw-r--r--libs/uibase/src/eventfilter.cpp34
-rw-r--r--libs/uibase/src/executableinfo.cpp105
-rw-r--r--libs/uibase/src/expanderwidget.cpp93
-rw-r--r--libs/uibase/src/filesystemutilities.cpp68
-rw-r--r--libs/uibase/src/filterwidget.cpp693
-rw-r--r--libs/uibase/src/finddialog.cpp51
-rw-r--r--libs/uibase/src/finddialog.ui76
-rw-r--r--libs/uibase/src/guessedvalue.cpp1
-rw-r--r--libs/uibase/src/ifiletree.cpp1029
-rw-r--r--libs/uibase/src/imodrepositorybridge.cpp1
-rw-r--r--libs/uibase/src/imoinfo.cpp23
-rw-r--r--libs/uibase/src/json.cpp557
-rw-r--r--libs/uibase/src/lineeditclear.cpp50
-rw-r--r--libs/uibase/src/linklabel.cpp27
-rw-r--r--libs/uibase/src/log.cpp411
-rw-r--r--libs/uibase/src/modrepositoryfileinfo.cpp81
-rw-r--r--libs/uibase/src/nxmurl.cpp44
-rw-r--r--libs/uibase/src/pch.cpp1
-rw-r--r--libs/uibase/src/pch.h139
-rw-r--r--libs/uibase/src/pluginrequirements.cpp138
-rw-r--r--libs/uibase/src/questionboxmemory.cpp200
-rw-r--r--libs/uibase/src/questionboxmemory.ui171
-rw-r--r--libs/uibase/src/registry.cpp189
-rw-r--r--libs/uibase/src/report.cpp453
-rw-r--r--libs/uibase/src/safewritefile.cpp84
-rw-r--r--libs/uibase/src/scopeguard.cpp6
-rw-r--r--libs/uibase/src/sortabletreewidget.cpp116
-rw-r--r--libs/uibase/src/steamutility.cpp108
-rw-r--r--libs/uibase/src/strings.cpp46
-rw-r--r--libs/uibase/src/taskdialog.ui359
-rw-r--r--libs/uibase/src/taskprogressmanager.cpp125
-rw-r--r--libs/uibase/src/textviewer.cpp278
-rw-r--r--libs/uibase/src/textviewer.ui55
-rw-r--r--libs/uibase/src/tutorabledialog.cpp40
-rw-r--r--libs/uibase/src/tutorialcontrol.cpp384
-rw-r--r--libs/uibase/src/tutorialmanager.cpp112
-rw-r--r--libs/uibase/src/uibase_en.ts407
-rw-r--r--libs/uibase/src/utility.cpp1260
-rw-r--r--libs/uibase/src/version.rc37
-rw-r--r--libs/uibase/src/versioninfo.cpp432
-rw-r--r--libs/uibase/src/versioning.cpp278
-rw-r--r--libs/uibase/src/widgetutility.cpp59
46 files changed, 11457 insertions, 0 deletions
diff --git a/libs/uibase/src/CMakeLists.txt b/libs/uibase/src/CMakeLists.txt
new file mode 100644
index 0000000..fff3ac3
--- /dev/null
+++ b/libs/uibase/src/CMakeLists.txt
@@ -0,0 +1,201 @@
+cmake_minimum_required(VERSION 3.16)
+
+find_package(Qt6 CONFIG REQUIRED COMPONENTS Network Qml Quick QuickWidgets Widgets)
+find_package(spdlog CONFIG REQUIRED)
+qt_standard_project_setup()
+
+set(root_headers
+ ../include/uibase/delayedfilewriter.h
+ ../include/uibase/diagnosisreport.h
+ ../include/uibase/dllimport.h
+ ../include/uibase/errorcodes.h
+ ../include/uibase/eventfilter.h
+ ../include/uibase/exceptions.h
+ ../include/uibase/executableinfo.h
+ ../include/uibase/filemapping.h
+ ../include/uibase/filesystemutilities.h
+ ../include/uibase/guessedvalue.h
+ ../include/uibase/idownloadmanager.h
+ ../include/uibase/json.h
+ ../include/uibase/log.h
+ ../include/uibase/memoizedlock.h
+ ../include/uibase/moassert.h
+ ../include/uibase/modrepositoryfileinfo.h
+ ../include/uibase/nxmurl.h
+ ../include/uibase/pluginrequirements.h
+ ../include/uibase/pluginsetting.h
+ ../include/uibase/registry.h
+ ../include/uibase/report.h
+ ../include/uibase/safewritefile.h
+ ../include/uibase/scopeguard.h
+ ../include/uibase/steamutility.h
+ ../include/uibase/strings.h
+ ../include/uibase/utility.h
+ ../include/uibase/versioning.h
+ ../include/uibase/versioninfo.h
+)
+set(interface_headers
+ ../include/uibase/iexecutable.h
+ ../include/uibase/iexecutableslist.h
+ ../include/uibase/ifiletree.h
+ ../include/uibase/ifiletree_utils.h
+ ../include/uibase/iinstallationmanager.h
+ ../include/uibase/iinstance.h
+ ../include/uibase/iinstancemanager.h
+ ../include/uibase/imodinterface.h
+ ../include/uibase/imodlist.h
+ ../include/uibase/imodrepositorybridge.h
+ ../include/uibase/imoinfo.h
+ ../include/uibase/iplugin.h
+ ../include/uibase/iplugindiagnose.h
+ ../include/uibase/ipluginfilemapper.h
+ ../include/uibase/iplugingame.h
+ ../include/uibase/iplugingamefeatures.h
+ ../include/uibase/iplugininstaller.h
+ ../include/uibase/iplugininstallercustom.h
+ ../include/uibase/iplugininstallersimple.h
+ ../include/uibase/ipluginlist.h
+ ../include/uibase/ipluginmodpage.h
+ ../include/uibase/ipluginpreview.h
+ ../include/uibase/ipluginproxy.h
+ ../include/uibase/iplugintool.h
+ ../include/uibase/iprofile.h
+ ../include/uibase/isavegame.h
+ ../include/uibase/isavegameinfowidget.h
+)
+set(tutorial_headers
+ ../include/uibase/tutorabledialog.h
+ ../include/uibase/tutorialcontrol.h
+ ../include/uibase/tutorialmanager.h
+)
+set(widget_headers
+ ../include/uibase/expanderwidget.h
+ ../include/uibase/filterwidget.h
+ ../include/uibase/finddialog.h
+ ../include/uibase/lineeditclear.h
+ ../include/uibase/linklabel.h
+ ../include/uibase/questionboxmemory.h
+ ../include/uibase/sortabletreewidget.h
+ ../include/uibase/taskprogressmanager.h
+ ../include/uibase/textviewer.h
+ ../include/uibase/widgetutility.h
+)
+set(game_features_header
+ ../include/uibase/game_features/bsainvalidation.h
+ ../include/uibase/game_features/dataarchives.h
+ ../include/uibase/game_features/game_feature.h
+ ../include/uibase/game_features/gameplugins.h
+ ../include/uibase/game_features/igamefeatures.h
+ ../include/uibase/game_features/localsavegames.h
+ ../include/uibase/game_features/moddatachecker.h
+ ../include/uibase/game_features/moddatacontent.h
+ ../include/uibase/game_features/savegameinfo.h
+ ../include/uibase/game_features/scriptextender.h
+ ../include/uibase/game_features/unmanagedmods.h
+)
+set(formatters_header
+ ../include/uibase/formatters/enums.h
+ ../include/uibase/formatters/qt.h
+ ../include/uibase/formatters/strings.h
+ ../include/uibase/formatters.h
+)
+
+# Collect all source files
+set(UIBASE_SOURCES
+ # root sources
+ delayedfilewriter.cpp
+ diagnosisreport.cpp
+ errorcodes.cpp
+ eventfilter.cpp
+ executableinfo.cpp
+ filesystemutilities.cpp
+ guessedvalue.cpp
+ json.cpp
+ log.cpp
+ modrepositoryfileinfo.cpp
+ nxmurl.cpp
+ pluginrequirements.cpp
+ registry.cpp
+ report.cpp
+ safewritefile.cpp
+ scopeguard.cpp
+ steamutility.cpp
+ strings.cpp
+ utility.cpp
+ versioning.cpp
+ versioninfo.cpp
+ # interfaces
+ ifiletree.cpp
+ imodrepositorybridge.cpp
+ imoinfo.cpp
+ # tutorials
+ tutorabledialog.cpp
+ tutorialcontrol.cpp
+ tutorialmanager.cpp
+ # widgets
+ expanderwidget.cpp
+ finddialog.cpp
+ lineeditclear.cpp
+ linklabel.cpp
+ questionboxmemory.cpp
+ sortabletreewidget.cpp
+ taskprogressmanager.cpp
+ textviewer.cpp
+ widgetutility.cpp
+ filterwidget.cpp
+)
+
+add_library(uibase SHARED
+ ${UIBASE_SOURCES}
+ ${root_headers}
+ ${interface_headers}
+ ${tutorial_headers}
+ ${widget_headers}
+ ${game_features_header}
+ ${formatters_header}
+ pch.h
+ finddialog.ui
+ questionboxmemory.ui
+ taskdialog.ui
+ textviewer.ui
+)
+
+set_target_properties(uibase PROPERTIES
+ DEBUG_POSTFIX d
+ AUTOMOC ON
+ AUTOUIC ON
+ CXX_STANDARD 23
+ CXX_STANDARD_REQUIRED ON
+ CXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
+ POSITION_INDEPENDENT_CODE ON
+)
+
+target_include_directories(uibase
+ PUBLIC
+ $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
+ PRIVATE
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
+)
+
+add_library(mo2::uibase ALIAS uibase)
+
+target_compile_definitions(uibase PRIVATE UIBASE_EXPORT SPDLOG_USE_STD_FORMAT)
+
+target_link_libraries(uibase
+ PUBLIC Qt6::Widgets Qt6::Network Qt6::QuickWidgets
+ PRIVATE spdlog::spdlog_header_only Qt6::Qml Qt6::Quick)
+
+# installation
+install(TARGETS uibase EXPORT uibaseTargets
+ LIBRARY DESTINATION lib
+ ARCHIVE DESTINATION lib
+ RUNTIME DESTINATION bin
+)
+install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/uibase DESTINATION include)
+install(EXPORT uibaseTargets
+ FILE mo2-uibase-targets.cmake
+ NAMESPACE mo2::
+ DESTINATION lib/cmake/mo2-uibase
+)
diff --git a/libs/uibase/src/delayedfilewriter.cpp b/libs/uibase/src/delayedfilewriter.cpp
new file mode 100644
index 0000000..9d4703a
--- /dev/null
+++ b/libs/uibase/src/delayedfilewriter.cpp
@@ -0,0 +1,49 @@
+#include <uibase/delayedfilewriter.h>
+#include <uibase/log.h>
+
+using namespace MOBase;
+
+DelayedFileWriterBase::DelayedFileWriterBase(int delay) : m_TimerDelay(delay), m_Timer()
+{
+ QObject::connect(&m_Timer, &QTimer::timeout, this, &DelayedFileWriter::timerExpired);
+ m_Timer.setSingleShot(true);
+}
+
+DelayedFileWriterBase::~DelayedFileWriterBase()
+{
+ if (m_Timer.isActive()) {
+ log::error("delayed file save timer active at shutdown");
+ }
+}
+
+void DelayedFileWriterBase::write()
+{
+ m_Timer.start(m_TimerDelay);
+}
+
+void DelayedFileWriterBase::cancel()
+{
+ m_Timer.stop();
+}
+
+void DelayedFileWriterBase::writeImmediately(bool ifOnTimer)
+{
+ if (!ifOnTimer || m_Timer.isActive()) {
+ m_Timer.stop();
+ doWrite();
+ }
+}
+
+void DelayedFileWriterBase::timerExpired()
+{
+ doWrite();
+}
+
+DelayedFileWriter::DelayedFileWriter(DelayedFileWriter::WriterFunc func, int delay)
+ : DelayedFileWriterBase(delay), m_Func(func)
+{}
+
+void DelayedFileWriter::doWrite()
+{
+ m_Func();
+}
diff --git a/libs/uibase/src/diagnosisreport.cpp b/libs/uibase/src/diagnosisreport.cpp
new file mode 100644
index 0000000..8a19a55
--- /dev/null
+++ b/libs/uibase/src/diagnosisreport.cpp
@@ -0,0 +1,25 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/diagnosisreport.h>
+
+namespace MOBase
+{
+} // namespace MOBase
diff --git a/libs/uibase/src/errorcodes.cpp b/libs/uibase/src/errorcodes.cpp
new file mode 100644
index 0000000..16c262a
--- /dev/null
+++ b/libs/uibase/src/errorcodes.cpp
@@ -0,0 +1,2361 @@
+#include <uibase/errorcodes.h>
+#include <algorithm>
+#include <iterator>
+
+namespace MOBase
+{
+
+struct CodeName
+{
+ DWORD code = 0;
+ const wchar_t* name = nullptr;
+};
+
+static const CodeName g_codes[] = {
+ {1, L"ERROR_INVALID_FUNCTION"},
+ {2, L"ERROR_FILE_NOT_FOUND"},
+ {3, L"ERROR_PATH_NOT_FOUND"},
+ {4, L"ERROR_TOO_MANY_OPEN_FILES"},
+ {5, L"ERROR_ACCESS_DENIED"},
+ {6, L"ERROR_INVALID_HANDLE"},
+ {7, L"ERROR_ARENA_TRASHED"},
+ {8, L"ERROR_NOT_ENOUGH_MEMORY"},
+ {9, L"ERROR_INVALID_BLOCK"},
+ {10, L"ERROR_BAD_ENVIRONMENT"},
+ {11, L"ERROR_BAD_FORMAT"},
+ {12, L"ERROR_INVALID_ACCESS"},
+ {13, L"ERROR_INVALID_DATA"},
+ {14, L"ERROR_OUTOFMEMORY"},
+ {15, L"ERROR_INVALID_DRIVE"},
+ {16, L"ERROR_CURRENT_DIRECTORY"},
+ {17, L"ERROR_NOT_SAME_DEVICE"},
+ {18, L"ERROR_NO_MORE_FILES"},
+ {19, L"ERROR_WRITE_PROTECT"},
+ {20, L"ERROR_BAD_UNIT"},
+ {21, L"ERROR_NOT_READY"},
+ {22, L"ERROR_BAD_COMMAND"},
+ {23, L"ERROR_CRC"},
+ {24, L"ERROR_BAD_LENGTH"},
+ {25, L"ERROR_SEEK"},
+ {26, L"ERROR_NOT_DOS_DISK"},
+ {27, L"ERROR_SECTOR_NOT_FOUND"},
+ {28, L"ERROR_OUT_OF_PAPER"},
+ {29, L"ERROR_WRITE_FAULT"},
+ {30, L"ERROR_READ_FAULT"},
+ {31, L"ERROR_GEN_FAILURE"},
+ {32, L"ERROR_SHARING_VIOLATION"},
+ {33, L"ERROR_LOCK_VIOLATION"},
+ {34, L"ERROR_WRONG_DISK"},
+ {36, L"ERROR_SHARING_BUFFER_EXCEEDED"},
+ {38, L"ERROR_HANDLE_EOF"},
+ {39, L"ERROR_HANDLE_DISK_FULL"},
+ {50, L"ERROR_NOT_SUPPORTED"},
+ {51, L"ERROR_REM_NOT_LIST"},
+ {52, L"ERROR_DUP_NAME"},
+ {53, L"ERROR_BAD_NETPATH"},
+ {54, L"ERROR_NETWORK_BUSY"},
+ {55, L"ERROR_DEV_NOT_EXIST"},
+ {56, L"ERROR_TOO_MANY_CMDS"},
+ {57, L"ERROR_ADAP_HDW_ERR"},
+ {58, L"ERROR_BAD_NET_RESP"},
+ {59, L"ERROR_UNEXP_NET_ERR"},
+ {60, L"ERROR_BAD_REM_ADAP"},
+ {61, L"ERROR_PRINTQ_FULL"},
+ {62, L"ERROR_NO_SPOOL_SPACE"},
+ {63, L"ERROR_PRINT_CANCELLED"},
+ {64, L"ERROR_NETNAME_DELETED"},
+ {65, L"ERROR_NETWORK_ACCESS_DENIED"},
+ {66, L"ERROR_BAD_DEV_TYPE"},
+ {67, L"ERROR_BAD_NET_NAME"},
+ {68, L"ERROR_TOO_MANY_NAMES"},
+ {69, L"ERROR_TOO_MANY_SESS"},
+ {70, L"ERROR_SHARING_PAUSED"},
+ {71, L"ERROR_REQ_NOT_ACCEP"},
+ {72, L"ERROR_REDIR_PAUSED"},
+ {80, L"ERROR_FILE_EXISTS"},
+ {82, L"ERROR_CANNOT_MAKE"},
+ {83, L"ERROR_FAIL_I24"},
+ {84, L"ERROR_OUT_OF_STRUCTURES"},
+ {85, L"ERROR_ALREADY_ASSIGNED"},
+ {86, L"ERROR_INVALID_PASSWORD"},
+ {87, L"ERROR_INVALID_PARAMETER"},
+ {88, L"ERROR_NET_WRITE_FAULT"},
+ {89, L"ERROR_NO_PROC_SLOTS"},
+ {100, L"ERROR_TOO_MANY_SEMAPHORES"},
+ {101, L"ERROR_EXCL_SEM_ALREADY_OWNED"},
+ {102, L"ERROR_SEM_IS_SET"},
+ {103, L"ERROR_TOO_MANY_SEM_REQUESTS"},
+ {104, L"ERROR_INVALID_AT_INTERRUPT_TIME"},
+ {105, L"ERROR_SEM_OWNER_DIED"},
+ {106, L"ERROR_SEM_USER_LIMIT"},
+ {107, L"ERROR_DISK_CHANGE"},
+ {108, L"ERROR_DRIVE_LOCKED"},
+ {109, L"ERROR_BROKEN_PIPE"},
+ {110, L"ERROR_OPEN_FAILED"},
+ {111, L"ERROR_BUFFER_OVERFLOW"},
+ {112, L"ERROR_DISK_FULL"},
+ {113, L"ERROR_NO_MORE_SEARCH_HANDLES"},
+ {114, L"ERROR_INVALID_TARGET_HANDLE"},
+ {117, L"ERROR_INVALID_CATEGORY"},
+ {118, L"ERROR_INVALID_VERIFY_SWITCH"},
+ {119, L"ERROR_BAD_DRIVER_LEVEL"},
+ {120, L"ERROR_CALL_NOT_IMPLEMENTED"},
+ {121, L"ERROR_SEM_TIMEOUT"},
+ {122, L"ERROR_INSUFFICIENT_BUFFER"},
+ {123, L"ERROR_INVALID_NAME"},
+ {124, L"ERROR_INVALID_LEVEL"},
+ {125, L"ERROR_NO_VOLUME_LABEL"},
+ {126, L"ERROR_MOD_NOT_FOUND"},
+ {127, L"ERROR_PROC_NOT_FOUND"},
+ {128, L"ERROR_WAIT_NO_CHILDREN"},
+ {129, L"ERROR_CHILD_NOT_COMPLETE"},
+ {130, L"ERROR_DIRECT_ACCESS_HANDLE"},
+ {131, L"ERROR_NEGATIVE_SEEK"},
+ {132, L"ERROR_SEEK_ON_DEVICE"},
+ {133, L"ERROR_IS_JOIN_TARGET"},
+ {134, L"ERROR_IS_JOINED"},
+ {135, L"ERROR_IS_SUBSTED"},
+ {136, L"ERROR_NOT_JOINED"},
+ {137, L"ERROR_NOT_SUBSTED"},
+ {138, L"ERROR_JOIN_TO_JOIN"},
+ {139, L"ERROR_SUBST_TO_SUBST"},
+ {140, L"ERROR_JOIN_TO_SUBST"},
+ {141, L"ERROR_SUBST_TO_JOIN"},
+ {142, L"ERROR_BUSY_DRIVE"},
+ {143, L"ERROR_SAME_DRIVE"},
+ {144, L"ERROR_DIR_NOT_ROOT"},
+ {145, L"ERROR_DIR_NOT_EMPTY"},
+ {146, L"ERROR_IS_SUBST_PATH"},
+ {147, L"ERROR_IS_JOIN_PATH"},
+ {148, L"ERROR_PATH_BUSY"},
+ {149, L"ERROR_IS_SUBST_TARGET"},
+ {150, L"ERROR_SYSTEM_TRACE"},
+ {151, L"ERROR_INVALID_EVENT_COUNT"},
+ {152, L"ERROR_TOO_MANY_MUXWAITERS"},
+ {153, L"ERROR_INVALID_LIST_FORMAT"},
+ {154, L"ERROR_LABEL_TOO_LONG"},
+ {155, L"ERROR_TOO_MANY_TCBS"},
+ {156, L"ERROR_SIGNAL_REFUSED"},
+ {157, L"ERROR_DISCARDED"},
+ {158, L"ERROR_NOT_LOCKED"},
+ {159, L"ERROR_BAD_THREADID_ADDR"},
+ {160, L"ERROR_BAD_ARGUMENTS"},
+ {161, L"ERROR_BAD_PATHNAME"},
+ {162, L"ERROR_SIGNAL_PENDING"},
+ {164, L"ERROR_MAX_THRDS_REACHED"},
+ {167, L"ERROR_LOCK_FAILED"},
+ {170, L"ERROR_BUSY"},
+ {171, L"ERROR_DEVICE_SUPPORT_IN_PROGRESS"},
+ {173, L"ERROR_CANCEL_VIOLATION"},
+ {174, L"ERROR_ATOMIC_LOCKS_NOT_SUPPORTED"},
+ {180, L"ERROR_INVALID_SEGMENT_NUMBER"},
+ {182, L"ERROR_INVALID_ORDINAL"},
+ {183, L"ERROR_ALREADY_EXISTS"},
+ {186, L"ERROR_INVALID_FLAG_NUMBER"},
+ {187, L"ERROR_SEM_NOT_FOUND"},
+ {188, L"ERROR_INVALID_STARTING_CODESEG"},
+ {189, L"ERROR_INVALID_STACKSEG"},
+ {190, L"ERROR_INVALID_MODULETYPE"},
+ {191, L"ERROR_INVALID_EXE_SIGNATURE"},
+ {192, L"ERROR_EXE_MARKED_INVALID"},
+ {193, L"ERROR_BAD_EXE_FORMAT"},
+ {194, L"ERROR_ITERATED_DATA_EXCEEDS_64k"},
+ {195, L"ERROR_INVALID_MINALLOCSIZE"},
+ {196, L"ERROR_DYNLINK_FROM_INVALID_RING"},
+ {197, L"ERROR_IOPL_NOT_ENABLED"},
+ {198, L"ERROR_INVALID_SEGDPL"},
+ {199, L"ERROR_AUTODATASEG_EXCEEDS_64k"},
+ {200, L"ERROR_RING2SEG_MUST_BE_MOVABLE"},
+ {201, L"ERROR_RELOC_CHAIN_XEEDS_SEGLIM"},
+ {202, L"ERROR_INFLOOP_IN_RELOC_CHAIN"},
+ {203, L"ERROR_ENVVAR_NOT_FOUND"},
+ {205, L"ERROR_NO_SIGNAL_SENT"},
+ {206, L"ERROR_FILENAME_EXCED_RANGE"},
+ {207, L"ERROR_RING2_STACK_IN_USE"},
+ {208, L"ERROR_META_EXPANSION_TOO_LONG"},
+ {209, L"ERROR_INVALID_SIGNAL_NUMBER"},
+ {210, L"ERROR_THREAD_1_INACTIVE"},
+ {212, L"ERROR_LOCKED"},
+ {214, L"ERROR_TOO_MANY_MODULES"},
+ {215, L"ERROR_NESTING_NOT_ALLOWED"},
+ {216, L"ERROR_EXE_MACHINE_TYPE_MISMATCH"},
+ {217, L"ERROR_EXE_CANNOT_MODIFY_SIGNED_BINARY"},
+ {218, L"ERROR_EXE_CANNOT_MODIFY_STRONG_SIGNED_BINARY"},
+ {220, L"ERROR_FILE_CHECKED_OUT"},
+ {221, L"ERROR_CHECKOUT_REQUIRED"},
+ {222, L"ERROR_BAD_FILE_TYPE"},
+ {223, L"ERROR_FILE_TOO_LARGE"},
+ {224, L"ERROR_FORMS_AUTH_REQUIRED"},
+ {225, L"ERROR_VIRUS_INFECTED"},
+ {226, L"ERROR_VIRUS_DELETED"},
+ {229, L"ERROR_PIPE_LOCAL"},
+ {230, L"ERROR_BAD_PIPE"},
+ {231, L"ERROR_PIPE_BUSY"},
+ {232, L"ERROR_NO_DATA"},
+ {233, L"ERROR_PIPE_NOT_CONNECTED"},
+ {234, L"ERROR_MORE_DATA"},
+ {240, L"ERROR_VC_DISCONNECTED"},
+ {254, L"ERROR_INVALID_EA_NAME"},
+ {255, L"ERROR_EA_LIST_INCONSISTENT"},
+ {258, L"WAIT_TIMEOUT"},
+ {259, L"ERROR_NO_MORE_ITEMS"},
+ {266, L"ERROR_CANNOT_COPY"},
+ {267, L"ERROR_DIRECTORY"},
+ {275, L"ERROR_EAS_DIDNT_FIT"},
+ {276, L"ERROR_EA_FILE_CORRUPT"},
+ {277, L"ERROR_EA_TABLE_FULL"},
+ {278, L"ERROR_INVALID_EA_HANDLE"},
+ {282, L"ERROR_EAS_NOT_SUPPORTED"},
+ {288, L"ERROR_NOT_OWNER"},
+ {298, L"ERROR_TOO_MANY_POSTS"},
+ {299, L"ERROR_PARTIAL_COPY"},
+ {300, L"ERROR_OPLOCK_NOT_GRANTED"},
+ {301, L"ERROR_INVALID_OPLOCK_PROTOCOL"},
+ {302, L"ERROR_DISK_TOO_FRAGMENTED"},
+ {303, L"ERROR_DELETE_PENDING"},
+ {304, L"ERROR_INCOMPATIBLE_WITH_GLOBAL_SHORT_NAME_REGISTRY_SETTING"},
+ {305, L"ERROR_SHORT_NAMES_NOT_ENABLED_ON_VOLUME"},
+ {306, L"ERROR_SECURITY_STREAM_IS_INCONSISTENT"},
+ {307, L"ERROR_INVALID_LOCK_RANGE"},
+ {308, L"ERROR_IMAGE_SUBSYSTEM_NOT_PRESENT"},
+ {309, L"ERROR_NOTIFICATION_GUID_ALREADY_DEFINED"},
+ {310, L"ERROR_INVALID_EXCEPTION_HANDLER"},
+ {311, L"ERROR_DUPLICATE_PRIVILEGES"},
+ {312, L"ERROR_NO_RANGES_PROCESSED"},
+ {313, L"ERROR_NOT_ALLOWED_ON_SYSTEM_FILE"},
+ {314, L"ERROR_DISK_RESOURCES_EXHAUSTED"},
+ {315, L"ERROR_INVALID_TOKEN"},
+ {316, L"ERROR_DEVICE_FEATURE_NOT_SUPPORTED"},
+ {317, L"ERROR_MR_MID_NOT_FOUND"},
+ {318, L"ERROR_SCOPE_NOT_FOUND"},
+ {319, L"ERROR_UNDEFINED_SCOPE"},
+ {320, L"ERROR_INVALID_CAP"},
+ {321, L"ERROR_DEVICE_UNREACHABLE"},
+ {322, L"ERROR_DEVICE_NO_RESOURCES"},
+ {323, L"ERROR_DATA_CHECKSUM_ERROR"},
+ {324, L"ERROR_INTERMIXED_KERNEL_EA_OPERATION"},
+ {326, L"ERROR_FILE_LEVEL_TRIM_NOT_SUPPORTED"},
+ {327, L"ERROR_OFFSET_ALIGNMENT_VIOLATION"},
+ {328, L"ERROR_INVALID_FIELD_IN_PARAMETER_LIST"},
+ {329, L"ERROR_OPERATION_IN_PROGRESS"},
+ {330, L"ERROR_BAD_DEVICE_PATH"},
+ {331, L"ERROR_TOO_MANY_DESCRIPTORS"},
+ {332, L"ERROR_SCRUB_DATA_DISABLED"},
+ {333, L"ERROR_NOT_REDUNDANT_STORAGE"},
+ {334, L"ERROR_RESIDENT_FILE_NOT_SUPPORTED"},
+ {335, L"ERROR_COMPRESSED_FILE_NOT_SUPPORTED"},
+ {336, L"ERROR_DIRECTORY_NOT_SUPPORTED"},
+ {337, L"ERROR_NOT_READ_FROM_COPY"},
+ {350, L"ERROR_FAIL_NOACTION_REBOOT"},
+ {351, L"ERROR_FAIL_SHUTDOWN"},
+ {352, L"ERROR_FAIL_RESTART"},
+ {353, L"ERROR_MAX_SESSIONS_REACHED"},
+ {400, L"ERROR_THREAD_MODE_ALREADY_BACKGROUND"},
+ {401, L"ERROR_THREAD_MODE_NOT_BACKGROUND"},
+ {402, L"ERROR_PROCESS_MODE_ALREADY_BACKGROUND"},
+ {403, L"ERROR_PROCESS_MODE_NOT_BACKGROUND"},
+ {487, L"ERROR_INVALID_ADDRESS"},
+ {500, L"ERROR_USER_PROFILE_LOAD"},
+ {534, L"ERROR_ARITHMETIC_OVERFLOW"},
+ {535, L"ERROR_PIPE_CONNECTED"},
+ {536, L"ERROR_PIPE_LISTENING"},
+ {537, L"ERROR_VERIFIER_STOP"},
+ {538, L"ERROR_ABIOS_ERROR"},
+ {539, L"ERROR_WX86_WARNING"},
+ {540, L"ERROR_WX86_ERROR"},
+ {541, L"ERROR_TIMER_NOT_CANCELED"},
+ {542, L"ERROR_UNWIND"},
+ {543, L"ERROR_BAD_STACK"},
+ {544, L"ERROR_INVALID_UNWIND_TARGET"},
+ {545, L"ERROR_INVALID_PORT_ATTRIBUTES"},
+ {546, L"ERROR_PORT_MESSAGE_TOO_LONG"},
+ {547, L"ERROR_INVALID_QUOTA_LOWER"},
+ {548, L"ERROR_DEVICE_ALREADY_ATTACHED"},
+ {549, L"ERROR_INSTRUCTION_MISALIGNMENT"},
+ {550, L"ERROR_PROFILING_NOT_STARTED"},
+ {551, L"ERROR_PROFILING_NOT_STOPPED"},
+ {552, L"ERROR_COULD_NOT_INTERPRET"},
+ {553, L"ERROR_PROFILING_AT_LIMIT"},
+ {554, L"ERROR_CANT_WAIT"},
+ {555, L"ERROR_CANT_TERMINATE_SELF"},
+ {556, L"ERROR_UNEXPECTED_MM_CREATE_ERR"},
+ {557, L"ERROR_UNEXPECTED_MM_MAP_ERROR"},
+ {558, L"ERROR_UNEXPECTED_MM_EXTEND_ERR"},
+ {559, L"ERROR_BAD_FUNCTION_TABLE"},
+ {560, L"ERROR_NO_GUID_TRANSLATION"},
+ {561, L"ERROR_INVALID_LDT_SIZE"},
+ {563, L"ERROR_INVALID_LDT_OFFSET"},
+ {564, L"ERROR_INVALID_LDT_DESCRIPTOR"},
+ {565, L"ERROR_TOO_MANY_THREADS"},
+ {566, L"ERROR_THREAD_NOT_IN_PROCESS"},
+ {567, L"ERROR_PAGEFILE_QUOTA_EXCEEDED"},
+ {568, L"ERROR_LOGON_SERVER_CONFLICT"},
+ {569, L"ERROR_SYNCHRONIZATION_REQUIRED"},
+ {570, L"ERROR_NET_OPEN_FAILED"},
+ {571, L"ERROR_IO_PRIVILEGE_FAILED"},
+ {572, L"ERROR_CONTROL_C_EXIT"},
+ {573, L"ERROR_MISSING_SYSTEMFILE"},
+ {574, L"ERROR_UNHANDLED_EXCEPTION"},
+ {575, L"ERROR_APP_INIT_FAILURE"},
+ {576, L"ERROR_PAGEFILE_CREATE_FAILED"},
+ {577, L"ERROR_INVALID_IMAGE_HASH"},
+ {578, L"ERROR_NO_PAGEFILE"},
+ {579, L"ERROR_ILLEGAL_FLOAT_CONTEXT"},
+ {580, L"ERROR_NO_EVENT_PAIR"},
+ {581, L"ERROR_DOMAIN_CTRLR_CONFIG_ERROR"},
+ {582, L"ERROR_ILLEGAL_CHARACTER"},
+ {583, L"ERROR_UNDEFINED_CHARACTER"},
+ {584, L"ERROR_FLOPPY_VOLUME"},
+ {585, L"ERROR_BIOS_FAILED_TO_CONNECT_INTERRUPT"},
+ {586, L"ERROR_BACKUP_CONTROLLER"},
+ {587, L"ERROR_MUTANT_LIMIT_EXCEEDED"},
+ {588, L"ERROR_FS_DRIVER_REQUIRED"},
+ {589, L"ERROR_CANNOT_LOAD_REGISTRY_FILE"},
+ {590, L"ERROR_DEBUG_ATTACH_FAILED"},
+ {591, L"ERROR_SYSTEM_PROCESS_TERMINATED"},
+ {592, L"ERROR_DATA_NOT_ACCEPTED"},
+ {593, L"ERROR_VDM_HARD_ERROR"},
+ {594, L"ERROR_DRIVER_CANCEL_TIMEOUT"},
+ {595, L"ERROR_REPLY_MESSAGE_MISMATCH"},
+ {596, L"ERROR_LOST_WRITEBEHIND_DATA"},
+ {597, L"ERROR_CLIENT_SERVER_PARAMETERS_INVALID"},
+ {598, L"ERROR_NOT_TINY_STREAM"},
+ {599, L"ERROR_STACK_OVERFLOW_READ"},
+ {600, L"ERROR_CONVERT_TO_LARGE"},
+ {601, L"ERROR_FOUND_OUT_OF_SCOPE"},
+ {602, L"ERROR_ALLOCATE_BUCKET"},
+ {603, L"ERROR_MARSHALL_OVERFLOW"},
+ {604, L"ERROR_INVALID_VARIANT"},
+ {605, L"ERROR_BAD_COMPRESSION_BUFFER"},
+ {606, L"ERROR_AUDIT_FAILED"},
+ {607, L"ERROR_TIMER_RESOLUTION_NOT_SET"},
+ {608, L"ERROR_INSUFFICIENT_LOGON_INFO"},
+ {609, L"ERROR_BAD_DLL_ENTRYPOINT"},
+ {610, L"ERROR_BAD_SERVICE_ENTRYPOINT"},
+ {611, L"ERROR_IP_ADDRESS_CONFLICT1"},
+ {612, L"ERROR_IP_ADDRESS_CONFLICT2"},
+ {613, L"ERROR_REGISTRY_QUOTA_LIMIT"},
+ {614, L"ERROR_NO_CALLBACK_ACTIVE"},
+ {615, L"ERROR_PWD_TOO_SHORT"},
+ {616, L"ERROR_PWD_TOO_RECENT"},
+ {617, L"ERROR_PWD_HISTORY_CONFLICT"},
+ {618, L"ERROR_UNSUPPORTED_COMPRESSION"},
+ {619, L"ERROR_INVALID_HW_PROFILE"},
+ {620, L"ERROR_INVALID_PLUGPLAY_DEVICE_PATH"},
+ {621, L"ERROR_QUOTA_LIST_INCONSISTENT"},
+ {622, L"ERROR_EVALUATION_EXPIRATION"},
+ {623, L"ERROR_ILLEGAL_DLL_RELOCATION"},
+ {624, L"ERROR_DLL_INIT_FAILED_LOGOFF"},
+ {625, L"ERROR_VALIDATE_CONTINUE"},
+ {626, L"ERROR_NO_MORE_MATCHES"},
+ {627, L"ERROR_RANGE_LIST_CONFLICT"},
+ {628, L"ERROR_SERVER_SID_MISMATCH"},
+ {629, L"ERROR_CANT_ENABLE_DENY_ONLY"},
+ {630, L"ERROR_FLOAT_MULTIPLE_FAULTS"},
+ {631, L"ERROR_FLOAT_MULTIPLE_TRAPS"},
+ {632, L"ERROR_NOINTERFACE"},
+ {633, L"ERROR_DRIVER_FAILED_SLEEP"},
+ {634, L"ERROR_CORRUPT_SYSTEM_FILE"},
+ {635, L"ERROR_COMMITMENT_MINIMUM"},
+ {636, L"ERROR_PNP_RESTART_ENUMERATION"},
+ {637, L"ERROR_SYSTEM_IMAGE_BAD_SIGNATURE"},
+ {638, L"ERROR_PNP_REBOOT_REQUIRED"},
+ {639, L"ERROR_INSUFFICIENT_POWER"},
+ {640, L"ERROR_MULTIPLE_FAULT_VIOLATION"},
+ {641, L"ERROR_SYSTEM_SHUTDOWN"},
+ {642, L"ERROR_PORT_NOT_SET"},
+ {643, L"ERROR_DS_VERSION_CHECK_FAILURE"},
+ {644, L"ERROR_RANGE_NOT_FOUND"},
+ {646, L"ERROR_NOT_SAFE_MODE_DRIVER"},
+ {647, L"ERROR_FAILED_DRIVER_ENTRY"},
+ {648, L"ERROR_DEVICE_ENUMERATION_ERROR"},
+ {649, L"ERROR_MOUNT_POINT_NOT_RESOLVED"},
+ {650, L"ERROR_INVALID_DEVICE_OBJECT_PARAMETER"},
+ {651, L"ERROR_MCA_OCCURED"},
+ {652, L"ERROR_DRIVER_DATABASE_ERROR"},
+ {653, L"ERROR_SYSTEM_HIVE_TOO_LARGE"},
+ {654, L"ERROR_DRIVER_FAILED_PRIOR_UNLOAD"},
+ {655, L"ERROR_VOLSNAP_PREPARE_HIBERNATE"},
+ {656, L"ERROR_HIBERNATION_FAILURE"},
+ {657, L"ERROR_PWD_TOO_LONG"},
+ {665, L"ERROR_FILE_SYSTEM_LIMITATION"},
+ {668, L"ERROR_ASSERTION_FAILURE"},
+ {669, L"ERROR_ACPI_ERROR"},
+ {670, L"ERROR_WOW_ASSERTION"},
+ {671, L"ERROR_PNP_BAD_MPS_TABLE"},
+ {672, L"ERROR_PNP_TRANSLATION_FAILED"},
+ {673, L"ERROR_PNP_IRQ_TRANSLATION_FAILED"},
+ {674, L"ERROR_PNP_INVALID_ID"},
+ {675, L"ERROR_WAKE_SYSTEM_DEBUGGER"},
+ {676, L"ERROR_HANDLES_CLOSED"},
+ {677, L"ERROR_EXTRANEOUS_INFORMATION"},
+ {678, L"ERROR_RXACT_COMMIT_NECESSARY"},
+ {679, L"ERROR_MEDIA_CHECK"},
+ {680, L"ERROR_GUID_SUBSTITUTION_MADE"},
+ {681, L"ERROR_STOPPED_ON_SYMLINK"},
+ {682, L"ERROR_LONGJUMP"},
+ {683, L"ERROR_PLUGPLAY_QUERY_VETOED"},
+ {684, L"ERROR_UNWIND_CONSOLIDATE"},
+ {685, L"ERROR_REGISTRY_HIVE_RECOVERED"},
+ {686, L"ERROR_DLL_MIGHT_BE_INSECURE"},
+ {687, L"ERROR_DLL_MIGHT_BE_INCOMPATIBLE"},
+ {688, L"ERROR_DBG_EXCEPTION_NOT_HANDLED"},
+ {689, L"ERROR_DBG_REPLY_LATER"},
+ {690, L"ERROR_DBG_UNABLE_TO_PROVIDE_HANDLE"},
+ {691, L"ERROR_DBG_TERMINATE_THREAD"},
+ {692, L"ERROR_DBG_TERMINATE_PROCESS"},
+ {693, L"ERROR_DBG_CONTROL_C"},
+ {694, L"ERROR_DBG_PRINTEXCEPTION_C"},
+ {695, L"ERROR_DBG_RIPEXCEPTION"},
+ {696, L"ERROR_DBG_CONTROL_BREAK"},
+ {697, L"ERROR_DBG_COMMAND_EXCEPTION"},
+ {698, L"ERROR_OBJECT_NAME_EXISTS"},
+ {699, L"ERROR_THREAD_WAS_SUSPENDED"},
+ {700, L"ERROR_IMAGE_NOT_AT_BASE"},
+ {701, L"ERROR_RXACT_STATE_CREATED"},
+ {702, L"ERROR_SEGMENT_NOTIFICATION"},
+ {703, L"ERROR_BAD_CURRENT_DIRECTORY"},
+ {704, L"ERROR_FT_READ_RECOVERY_FROM_BACKUP"},
+ {705, L"ERROR_FT_WRITE_RECOVERY"},
+ {706, L"ERROR_IMAGE_MACHINE_TYPE_MISMATCH"},
+ {707, L"ERROR_RECEIVE_PARTIAL"},
+ {708, L"ERROR_RECEIVE_EXPEDITED"},
+ {709, L"ERROR_RECEIVE_PARTIAL_EXPEDITED"},
+ {710, L"ERROR_EVENT_DONE"},
+ {711, L"ERROR_EVENT_PENDING"},
+ {712, L"ERROR_CHECKING_FILE_SYSTEM"},
+ {713, L"ERROR_FATAL_APP_EXIT"},
+ {714, L"ERROR_PREDEFINED_HANDLE"},
+ {715, L"ERROR_WAS_UNLOCKED"},
+ {716, L"ERROR_SERVICE_NOTIFICATION"},
+ {717, L"ERROR_WAS_LOCKED"},
+ {718, L"ERROR_LOG_HARD_ERROR"},
+ {719, L"ERROR_ALREADY_WIN32"},
+ {720, L"ERROR_IMAGE_MACHINE_TYPE_MISMATCH_EXE"},
+ {721, L"ERROR_NO_YIELD_PERFORMED"},
+ {722, L"ERROR_TIMER_RESUME_IGNORED"},
+ {723, L"ERROR_ARBITRATION_UNHANDLED"},
+ {724, L"ERROR_CARDBUS_NOT_SUPPORTED"},
+ {725, L"ERROR_MP_PROCESSOR_MISMATCH"},
+ {726, L"ERROR_HIBERNATED"},
+ {727, L"ERROR_RESUME_HIBERNATION"},
+ {728, L"ERROR_FIRMWARE_UPDATED"},
+ {729, L"ERROR_DRIVERS_LEAKING_LOCKED_PAGES"},
+ {730, L"ERROR_WAKE_SYSTEM"},
+ {731, L"ERROR_WAIT_1"},
+ {732, L"ERROR_WAIT_2"},
+ {733, L"ERROR_WAIT_3"},
+ {734, L"ERROR_WAIT_63"},
+ {735, L"ERROR_ABANDONED_WAIT_0"},
+ {736, L"ERROR_ABANDONED_WAIT_63"},
+ {737, L"ERROR_USER_APC"},
+ {738, L"ERROR_KERNEL_APC"},
+ {739, L"ERROR_ALERTED"},
+ {740, L"ERROR_ELEVATION_REQUIRED"},
+ {741, L"ERROR_REPARSE"},
+ {742, L"ERROR_OPLOCK_BREAK_IN_PROGRESS"},
+ {743, L"ERROR_VOLUME_MOUNTED"},
+ {744, L"ERROR_RXACT_COMMITTED"},
+ {745, L"ERROR_NOTIFY_CLEANUP"},
+ {746, L"ERROR_PRIMARY_TRANSPORT_CONNECT_FAILED"},
+ {747, L"ERROR_PAGE_FAULT_TRANSITION"},
+ {748, L"ERROR_PAGE_FAULT_DEMAND_ZERO"},
+ {749, L"ERROR_PAGE_FAULT_COPY_ON_WRITE"},
+ {750, L"ERROR_PAGE_FAULT_GUARD_PAGE"},
+ {751, L"ERROR_PAGE_FAULT_PAGING_FILE"},
+ {752, L"ERROR_CACHE_PAGE_LOCKED"},
+ {753, L"ERROR_CRASH_DUMP"},
+ {754, L"ERROR_BUFFER_ALL_ZEROS"},
+ {755, L"ERROR_REPARSE_OBJECT"},
+ {756, L"ERROR_RESOURCE_REQUIREMENTS_CHANGED"},
+ {757, L"ERROR_TRANSLATION_COMPLETE"},
+ {758, L"ERROR_NOTHING_TO_TERMINATE"},
+ {759, L"ERROR_PROCESS_NOT_IN_JOB"},
+ {760, L"ERROR_PROCESS_IN_JOB"},
+ {761, L"ERROR_VOLSNAP_HIBERNATE_READY"},
+ {762, L"ERROR_FSFILTER_OP_COMPLETED_SUCCESSFULLY"},
+ {763, L"ERROR_INTERRUPT_VECTOR_ALREADY_CONNECTED"},
+ {764, L"ERROR_INTERRUPT_STILL_CONNECTED"},
+ {765, L"ERROR_WAIT_FOR_OPLOCK"},
+ {766, L"ERROR_DBG_EXCEPTION_HANDLED"},
+ {767, L"ERROR_DBG_CONTINUE"},
+ {768, L"ERROR_CALLBACK_POP_STACK"},
+ {769, L"ERROR_COMPRESSION_DISABLED"},
+ {770, L"ERROR_CANTFETCHBACKWARDS"},
+ {771, L"ERROR_CANTSCROLLBACKWARDS"},
+ {772, L"ERROR_ROWSNOTRELEASED"},
+ {773, L"ERROR_BAD_ACCESSOR_FLAGS"},
+ {774, L"ERROR_ERRORS_ENCOUNTERED"},
+ {775, L"ERROR_NOT_CAPABLE"},
+ {776, L"ERROR_REQUEST_OUT_OF_SEQUENCE"},
+ {777, L"ERROR_VERSION_PARSE_ERROR"},
+ {778, L"ERROR_BADSTARTPOSITION"},
+ {779, L"ERROR_MEMORY_HARDWARE"},
+ {780, L"ERROR_DISK_REPAIR_DISABLED"},
+ {781, L"ERROR_INSUFFICIENT_RESOURCE_FOR_SPECIFIED_SHARED_SECTION_SIZE"},
+ {782, L"ERROR_SYSTEM_POWERSTATE_TRANSITION"},
+ {783, L"ERROR_SYSTEM_POWERSTATE_COMPLEX_TRANSITION"},
+ {784, L"ERROR_MCA_EXCEPTION"},
+ {785, L"ERROR_ACCESS_AUDIT_BY_POLICY"},
+ {786, L"ERROR_ACCESS_DISABLED_NO_SAFER_UI_BY_POLICY"},
+ {787, L"ERROR_ABANDON_HIBERFILE"},
+ {788, L"ERROR_LOST_WRITEBEHIND_DATA_NETWORK_DISCONNECTED"},
+ {789, L"ERROR_LOST_WRITEBEHIND_DATA_NETWORK_SERVER_ERROR"},
+ {790, L"ERROR_LOST_WRITEBEHIND_DATA_LOCAL_DISK_ERROR"},
+ {791, L"ERROR_BAD_MCFG_TABLE"},
+ {792, L"ERROR_DISK_REPAIR_REDIRECTED"},
+ {793, L"ERROR_DISK_REPAIR_UNSUCCESSFUL"},
+ {794, L"ERROR_CORRUPT_LOG_OVERFULL"},
+ {795, L"ERROR_CORRUPT_LOG_CORRUPTED"},
+ {796, L"ERROR_CORRUPT_LOG_UNAVAILABLE"},
+ {797, L"ERROR_CORRUPT_LOG_DELETED_FULL"},
+ {798, L"ERROR_CORRUPT_LOG_CLEARED"},
+ {799, L"ERROR_ORPHAN_NAME_EXHAUSTED"},
+ {800, L"ERROR_OPLOCK_SWITCHED_TO_NEW_HANDLE"},
+ {801, L"ERROR_CANNOT_GRANT_REQUESTED_OPLOCK"},
+ {802, L"ERROR_CANNOT_BREAK_OPLOCK"},
+ {803, L"ERROR_OPLOCK_HANDLE_CLOSED"},
+ {804, L"ERROR_NO_ACE_CONDITION"},
+ {805, L"ERROR_INVALID_ACE_CONDITION"},
+ {806, L"ERROR_FILE_HANDLE_REVOKED"},
+ {807, L"ERROR_IMAGE_AT_DIFFERENT_BASE"},
+ {994, L"ERROR_EA_ACCESS_DENIED"},
+ {995, L"ERROR_OPERATION_ABORTED"},
+ {996, L"ERROR_IO_INCOMPLETE"},
+ {997, L"ERROR_IO_PENDING"},
+ {998, L"ERROR_NOACCESS"},
+ {999, L"ERROR_SWAPERROR"},
+ {1001, L"ERROR_STACK_OVERFLOW"},
+ {1002, L"ERROR_INVALID_MESSAGE"},
+ {1003, L"ERROR_CAN_NOT_COMPLETE"},
+ {1004, L"ERROR_INVALID_FLAGS"},
+ {1005, L"ERROR_UNRECOGNIZED_VOLUME"},
+ {1006, L"ERROR_FILE_INVALID"},
+ {1007, L"ERROR_FULLSCREEN_MODE"},
+ {1008, L"ERROR_NO_TOKEN"},
+ {1009, L"ERROR_BADDB"},
+ {1010, L"ERROR_BADKEY"},
+ {1011, L"ERROR_CANTOPEN"},
+ {1012, L"ERROR_CANTREAD"},
+ {1013, L"ERROR_CANTWRITE"},
+ {1014, L"ERROR_REGISTRY_RECOVERED"},
+ {1015, L"ERROR_REGISTRY_CORRUPT"},
+ {1016, L"ERROR_REGISTRY_IO_FAILED"},
+ {1017, L"ERROR_NOT_REGISTRY_FILE"},
+ {1018, L"ERROR_KEY_DELETED"},
+ {1019, L"ERROR_NO_LOG_SPACE"},
+ {1020, L"ERROR_KEY_HAS_CHILDREN"},
+ {1021, L"ERROR_CHILD_MUST_BE_VOLATILE"},
+ {1022, L"ERROR_NOTIFY_ENUM_DIR"},
+ {1051, L"ERROR_DEPENDENT_SERVICES_RUNNING"},
+ {1052, L"ERROR_INVALID_SERVICE_CONTROL"},
+ {1053, L"ERROR_SERVICE_REQUEST_TIMEOUT"},
+ {1054, L"ERROR_SERVICE_NO_THREAD"},
+ {1055, L"ERROR_SERVICE_DATABASE_LOCKED"},
+ {1056, L"ERROR_SERVICE_ALREADY_RUNNING"},
+ {1057, L"ERROR_INVALID_SERVICE_ACCOUNT"},
+ {1058, L"ERROR_SERVICE_DISABLED"},
+ {1059, L"ERROR_CIRCULAR_DEPENDENCY"},
+ {1060, L"ERROR_SERVICE_DOES_NOT_EXIST"},
+ {1061, L"ERROR_SERVICE_CANNOT_ACCEPT_CTRL"},
+ {1062, L"ERROR_SERVICE_NOT_ACTIVE"},
+ {1063, L"ERROR_FAILED_SERVICE_CONTROLLER_CONNECT"},
+ {1064, L"ERROR_EXCEPTION_IN_SERVICE"},
+ {1065, L"ERROR_DATABASE_DOES_NOT_EXIST"},
+ {1066, L"ERROR_SERVICE_SPECIFIC_ERROR"},
+ {1067, L"ERROR_PROCESS_ABORTED"},
+ {1068, L"ERROR_SERVICE_DEPENDENCY_FAIL"},
+ {1069, L"ERROR_SERVICE_LOGON_FAILED"},
+ {1070, L"ERROR_SERVICE_START_HANG"},
+ {1071, L"ERROR_INVALID_SERVICE_LOCK"},
+ {1072, L"ERROR_SERVICE_MARKED_FOR_DELETE"},
+ {1073, L"ERROR_SERVICE_EXISTS"},
+ {1074, L"ERROR_ALREADY_RUNNING_LKG"},
+ {1075, L"ERROR_SERVICE_DEPENDENCY_DELETED"},
+ {1076, L"ERROR_BOOT_ALREADY_ACCEPTED"},
+ {1077, L"ERROR_SERVICE_NEVER_STARTED"},
+ {1078, L"ERROR_DUPLICATE_SERVICE_NAME"},
+ {1079, L"ERROR_DIFFERENT_SERVICE_ACCOUNT"},
+ {1080, L"ERROR_CANNOT_DETECT_DRIVER_FAILURE"},
+ {1081, L"ERROR_CANNOT_DETECT_PROCESS_ABORT"},
+ {1082, L"ERROR_NO_RECOVERY_PROGRAM"},
+ {1083, L"ERROR_SERVICE_NOT_IN_EXE"},
+ {1084, L"ERROR_NOT_SAFEBOOT_SERVICE"},
+ {1100, L"ERROR_END_OF_MEDIA"},
+ {1101, L"ERROR_FILEMARK_DETECTED"},
+ {1102, L"ERROR_BEGINNING_OF_MEDIA"},
+ {1103, L"ERROR_SETMARK_DETECTED"},
+ {1104, L"ERROR_NO_DATA_DETECTED"},
+ {1105, L"ERROR_PARTITION_FAILURE"},
+ {1106, L"ERROR_INVALID_BLOCK_LENGTH"},
+ {1107, L"ERROR_DEVICE_NOT_PARTITIONED"},
+ {1108, L"ERROR_UNABLE_TO_LOCK_MEDIA"},
+ {1109, L"ERROR_UNABLE_TO_UNLOAD_MEDIA"},
+ {1110, L"ERROR_MEDIA_CHANGED"},
+ {1111, L"ERROR_BUS_RESET"},
+ {1112, L"ERROR_NO_MEDIA_IN_DRIVE"},
+ {1113, L"ERROR_NO_UNICODE_TRANSLATION"},
+ {1114, L"ERROR_DLL_INIT_FAILED"},
+ {1115, L"ERROR_SHUTDOWN_IN_PROGRESS"},
+ {1116, L"ERROR_NO_SHUTDOWN_IN_PROGRESS"},
+ {1117, L"ERROR_IO_DEVICE"},
+ {1118, L"ERROR_SERIAL_NO_DEVICE"},
+ {1119, L"ERROR_IRQ_BUSY"},
+ {1120, L"ERROR_MORE_WRITES"},
+ {1121, L"ERROR_COUNTER_TIMEOUT"},
+ {1122, L"ERROR_FLOPPY_ID_MARK_NOT_FOUND"},
+ {1123, L"ERROR_FLOPPY_WRONG_CYLINDER"},
+ {1124, L"ERROR_FLOPPY_UNKNOWN_ERROR"},
+ {1125, L"ERROR_FLOPPY_BAD_REGISTERS"},
+ {1126, L"ERROR_DISK_RECALIBRATE_FAILED"},
+ {1127, L"ERROR_DISK_OPERATION_FAILED"},
+ {1128, L"ERROR_DISK_RESET_FAILED"},
+ {1129, L"ERROR_EOM_OVERFLOW"},
+ {1130, L"ERROR_NOT_ENOUGH_SERVER_MEMORY"},
+ {1131, L"ERROR_POSSIBLE_DEADLOCK"},
+ {1132, L"ERROR_MAPPED_ALIGNMENT"},
+ {1140, L"ERROR_SET_POWER_STATE_VETOED"},
+ {1141, L"ERROR_SET_POWER_STATE_FAILED"},
+ {1142, L"ERROR_TOO_MANY_LINKS"},
+ {1150, L"ERROR_OLD_WIN_VERSION"},
+ {1151, L"ERROR_APP_WRONG_OS"},
+ {1152, L"ERROR_SINGLE_INSTANCE_APP"},
+ {1153, L"ERROR_RMODE_APP"},
+ {1154, L"ERROR_INVALID_DLL"},
+ {1155, L"ERROR_NO_ASSOCIATION"},
+ {1156, L"ERROR_DDE_FAIL"},
+ {1157, L"ERROR_DLL_NOT_FOUND"},
+ {1158, L"ERROR_NO_MORE_USER_HANDLES"},
+ {1159, L"ERROR_MESSAGE_SYNC_ONLY"},
+ {1160, L"ERROR_SOURCE_ELEMENT_EMPTY"},
+ {1161, L"ERROR_DESTINATION_ELEMENT_FULL"},
+ {1162, L"ERROR_ILLEGAL_ELEMENT_ADDRESS"},
+ {1163, L"ERROR_MAGAZINE_NOT_PRESENT"},
+ {1164, L"ERROR_DEVICE_REINITIALIZATION_NEEDED"},
+ {1165, L"ERROR_DEVICE_REQUIRES_CLEANING"},
+ {1166, L"ERROR_DEVICE_DOOR_OPEN"},
+ {1167, L"ERROR_DEVICE_NOT_CONNECTED"},
+ {1168, L"ERROR_NOT_FOUND"},
+ {1169, L"ERROR_NO_MATCH"},
+ {1170, L"ERROR_SET_NOT_FOUND"},
+ {1171, L"ERROR_POINT_NOT_FOUND"},
+ {1172, L"ERROR_NO_TRACKING_SERVICE"},
+ {1173, L"ERROR_NO_VOLUME_ID"},
+ {1175, L"ERROR_UNABLE_TO_REMOVE_REPLACED"},
+ {1176, L"ERROR_UNABLE_TO_MOVE_REPLACEMENT"},
+ {1177, L"ERROR_UNABLE_TO_MOVE_REPLACEMENT_2"},
+ {1178, L"ERROR_JOURNAL_DELETE_IN_PROGRESS"},
+ {1179, L"ERROR_JOURNAL_NOT_ACTIVE"},
+ {1180, L"ERROR_POTENTIAL_FILE_FOUND"},
+ {1181, L"ERROR_JOURNAL_ENTRY_DELETED"},
+ {1190, L"ERROR_SHUTDOWN_IS_SCHEDULED"},
+ {1191, L"ERROR_SHUTDOWN_USERS_LOGGED_ON"},
+ {1200, L"ERROR_BAD_DEVICE"},
+ {1201, L"ERROR_CONNECTION_UNAVAIL"},
+ {1202, L"ERROR_DEVICE_ALREADY_REMEMBERED"},
+ {1203, L"ERROR_NO_NET_OR_BAD_PATH"},
+ {1204, L"ERROR_BAD_PROVIDER"},
+ {1205, L"ERROR_CANNOT_OPEN_PROFILE"},
+ {1206, L"ERROR_BAD_PROFILE"},
+ {1207, L"ERROR_NOT_CONTAINER"},
+ {1208, L"ERROR_EXTENDED_ERROR"},
+ {1209, L"ERROR_INVALID_GROUPNAME"},
+ {1210, L"ERROR_INVALID_COMPUTERNAME"},
+ {1211, L"ERROR_INVALID_EVENTNAME"},
+ {1212, L"ERROR_INVALID_DOMAINNAME"},
+ {1213, L"ERROR_INVALID_SERVICENAME"},
+ {1214, L"ERROR_INVALID_NETNAME"},
+ {1215, L"ERROR_INVALID_SHARENAME"},
+ {1216, L"ERROR_INVALID_PASSWORDNAME"},
+ {1217, L"ERROR_INVALID_MESSAGENAME"},
+ {1218, L"ERROR_INVALID_MESSAGEDEST"},
+ {1219, L"ERROR_SESSION_CREDENTIAL_CONFLICT"},
+ {1220, L"ERROR_REMOTE_SESSION_LIMIT_EXCEEDED"},
+ {1221, L"ERROR_DUP_DOMAINNAME"},
+ {1222, L"ERROR_NO_NETWORK"},
+ {1223, L"ERROR_CANCELLED"},
+ {1224, L"ERROR_USER_MAPPED_FILE"},
+ {1225, L"ERROR_CONNECTION_REFUSED"},
+ {1226, L"ERROR_GRACEFUL_DISCONNECT"},
+ {1227, L"ERROR_ADDRESS_ALREADY_ASSOCIATED"},
+ {1228, L"ERROR_ADDRESS_NOT_ASSOCIATED"},
+ {1229, L"ERROR_CONNECTION_INVALID"},
+ {1230, L"ERROR_CONNECTION_ACTIVE"},
+ {1231, L"ERROR_NETWORK_UNREACHABLE"},
+ {1232, L"ERROR_HOST_UNREACHABLE"},
+ {1233, L"ERROR_PROTOCOL_UNREACHABLE"},
+ {1234, L"ERROR_PORT_UNREACHABLE"},
+ {1235, L"ERROR_REQUEST_ABORTED"},
+ {1236, L"ERROR_CONNECTION_ABORTED"},
+ {1237, L"ERROR_RETRY"},
+ {1238, L"ERROR_CONNECTION_COUNT_LIMIT"},
+ {1239, L"ERROR_LOGIN_TIME_RESTRICTION"},
+ {1240, L"ERROR_LOGIN_WKSTA_RESTRICTION"},
+ {1241, L"ERROR_INCORRECT_ADDRESS"},
+ {1242, L"ERROR_ALREADY_REGISTERED"},
+ {1243, L"ERROR_SERVICE_NOT_FOUND"},
+ {1244, L"ERROR_NOT_AUTHENTICATED"},
+ {1245, L"ERROR_NOT_LOGGED_ON"},
+ {1246, L"ERROR_CONTINUE"},
+ {1247, L"ERROR_ALREADY_INITIALIZED"},
+ {1248, L"ERROR_NO_MORE_DEVICES"},
+ {1249, L"ERROR_NO_SUCH_SITE"},
+ {1250, L"ERROR_DOMAIN_CONTROLLER_EXISTS"},
+ {1251, L"ERROR_ONLY_IF_CONNECTED"},
+ {1252, L"ERROR_OVERRIDE_NOCHANGES"},
+ {1253, L"ERROR_BAD_USER_PROFILE"},
+ {1254, L"ERROR_NOT_SUPPORTED_ON_SBS"},
+ {1255, L"ERROR_SERVER_SHUTDOWN_IN_PROGRESS"},
+ {1256, L"ERROR_HOST_DOWN"},
+ {1257, L"ERROR_NON_ACCOUNT_SID"},
+ {1258, L"ERROR_NON_DOMAIN_SID"},
+ {1259, L"ERROR_APPHELP_BLOCK"},
+ {1260, L"ERROR_ACCESS_DISABLED_BY_POLICY"},
+ {1261, L"ERROR_REG_NAT_CONSUMPTION"},
+ {1262, L"ERROR_CSCSHARE_OFFLINE"},
+ {1263, L"ERROR_PKINIT_FAILURE"},
+ {1264, L"ERROR_SMARTCARD_SUBSYSTEM_FAILURE"},
+ {1265, L"ERROR_DOWNGRADE_DETECTED"},
+ {1271, L"ERROR_MACHINE_LOCKED"},
+ {1273, L"ERROR_CALLBACK_SUPPLIED_INVALID_DATA"},
+ {1274, L"ERROR_SYNC_FOREGROUND_REFRESH_REQUIRED"},
+ {1275, L"ERROR_DRIVER_BLOCKED"},
+ {1276, L"ERROR_INVALID_IMPORT_OF_NON_DLL"},
+ {1277, L"ERROR_ACCESS_DISABLED_WEBBLADE"},
+ {1278, L"ERROR_ACCESS_DISABLED_WEBBLADE_TAMPER"},
+ {1279, L"ERROR_RECOVERY_FAILURE"},
+ {1280, L"ERROR_ALREADY_FIBER"},
+ {1281, L"ERROR_ALREADY_THREAD"},
+ {1282, L"ERROR_STACK_BUFFER_OVERRUN"},
+ {1283, L"ERROR_PARAMETER_QUOTA_EXCEEDED"},
+ {1284, L"ERROR_DEBUGGER_INACTIVE"},
+ {1285, L"ERROR_DELAY_LOAD_FAILED"},
+ {1286, L"ERROR_VDM_DISALLOWED"},
+ {1287, L"ERROR_UNIDENTIFIED_ERROR"},
+ {1288, L"ERROR_INVALID_CRUNTIME_PARAMETER"},
+ {1289, L"ERROR_BEYOND_VDL"},
+ {1290, L"ERROR_INCOMPATIBLE_SERVICE_SID_TYPE"},
+ {1291, L"ERROR_DRIVER_PROCESS_TERMINATED"},
+ {1292, L"ERROR_IMPLEMENTATION_LIMIT"},
+ {1293, L"ERROR_PROCESS_IS_PROTECTED"},
+ {1294, L"ERROR_SERVICE_NOTIFY_CLIENT_LAGGING"},
+ {1295, L"ERROR_DISK_QUOTA_EXCEEDED"},
+ {1296, L"ERROR_CONTENT_BLOCKED"},
+ {1297, L"ERROR_INCOMPATIBLE_SERVICE_PRIVILEGE"},
+ {1298, L"ERROR_APP_HANG"},
+ {1299, L"ERROR_INVALID_LABEL"},
+ {1300, L"ERROR_NOT_ALL_ASSIGNED"},
+ {1301, L"ERROR_SOME_NOT_MAPPED"},
+ {1302, L"ERROR_NO_QUOTAS_FOR_ACCOUNT"},
+ {1303, L"ERROR_LOCAL_USER_SESSION_KEY"},
+ {1304, L"ERROR_NULL_LM_PASSWORD"},
+ {1305, L"ERROR_UNKNOWN_REVISION"},
+ {1306, L"ERROR_REVISION_MISMATCH"},
+ {1307, L"ERROR_INVALID_OWNER"},
+ {1308, L"ERROR_INVALID_PRIMARY_GROUP"},
+ {1309, L"ERROR_NO_IMPERSONATION_TOKEN"},
+ {1310, L"ERROR_CANT_DISABLE_MANDATORY"},
+ {1311, L"ERROR_NO_LOGON_SERVERS"},
+ {1312, L"ERROR_NO_SUCH_LOGON_SESSION"},
+ {1313, L"ERROR_NO_SUCH_PRIVILEGE"},
+ {1314, L"ERROR_PRIVILEGE_NOT_HELD"},
+ {1315, L"ERROR_INVALID_ACCOUNT_NAME"},
+ {1316, L"ERROR_USER_EXISTS"},
+ {1317, L"ERROR_NO_SUCH_USER"},
+ {1318, L"ERROR_GROUP_EXISTS"},
+ {1319, L"ERROR_NO_SUCH_GROUP"},
+ {1320, L"ERROR_MEMBER_IN_GROUP"},
+ {1321, L"ERROR_MEMBER_NOT_IN_GROUP"},
+ {1322, L"ERROR_LAST_ADMIN"},
+ {1323, L"ERROR_WRONG_PASSWORD"},
+ {1324, L"ERROR_ILL_FORMED_PASSWORD"},
+ {1325, L"ERROR_PASSWORD_RESTRICTION"},
+ {1326, L"ERROR_LOGON_FAILURE"},
+ {1327, L"ERROR_ACCOUNT_RESTRICTION"},
+ {1328, L"ERROR_INVALID_LOGON_HOURS"},
+ {1329, L"ERROR_INVALID_WORKSTATION"},
+ {1330, L"ERROR_PASSWORD_EXPIRED"},
+ {1331, L"ERROR_ACCOUNT_DISABLED"},
+ {1332, L"ERROR_NONE_MAPPED"},
+ {1333, L"ERROR_TOO_MANY_LUIDS_REQUESTED"},
+ {1334, L"ERROR_LUIDS_EXHAUSTED"},
+ {1335, L"ERROR_INVALID_SUB_AUTHORITY"},
+ {1336, L"ERROR_INVALID_ACL"},
+ {1337, L"ERROR_INVALID_SID"},
+ {1338, L"ERROR_INVALID_SECURITY_DESCR"},
+ {1340, L"ERROR_BAD_INHERITANCE_ACL"},
+ {1341, L"ERROR_SERVER_DISABLED"},
+ {1342, L"ERROR_SERVER_NOT_DISABLED"},
+ {1343, L"ERROR_INVALID_ID_AUTHORITY"},
+ {1344, L"ERROR_ALLOTTED_SPACE_EXCEEDED"},
+ {1345, L"ERROR_INVALID_GROUP_ATTRIBUTES"},
+ {1346, L"ERROR_BAD_IMPERSONATION_LEVEL"},
+ {1347, L"ERROR_CANT_OPEN_ANONYMOUS"},
+ {1348, L"ERROR_BAD_VALIDATION_CLASS"},
+ {1349, L"ERROR_BAD_TOKEN_TYPE"},
+ {1350, L"ERROR_NO_SECURITY_ON_OBJECT"},
+ {1351, L"ERROR_CANT_ACCESS_DOMAIN_INFO"},
+ {1352, L"ERROR_INVALID_SERVER_STATE"},
+ {1353, L"ERROR_INVALID_DOMAIN_STATE"},
+ {1354, L"ERROR_INVALID_DOMAIN_ROLE"},
+ {1355, L"ERROR_NO_SUCH_DOMAIN"},
+ {1356, L"ERROR_DOMAIN_EXISTS"},
+ {1357, L"ERROR_DOMAIN_LIMIT_EXCEEDED"},
+ {1358, L"ERROR_INTERNAL_DB_CORRUPTION"},
+ {1359, L"ERROR_INTERNAL_ERROR"},
+ {1360, L"ERROR_GENERIC_NOT_MAPPED"},
+ {1361, L"ERROR_BAD_DESCRIPTOR_FORMAT"},
+ {1362, L"ERROR_NOT_LOGON_PROCESS"},
+ {1363, L"ERROR_LOGON_SESSION_EXISTS"},
+ {1364, L"ERROR_NO_SUCH_PACKAGE"},
+ {1365, L"ERROR_BAD_LOGON_SESSION_STATE"},
+ {1366, L"ERROR_LOGON_SESSION_COLLISION"},
+ {1367, L"ERROR_INVALID_LOGON_TYPE"},
+ {1368, L"ERROR_CANNOT_IMPERSONATE"},
+ {1369, L"ERROR_RXACT_INVALID_STATE"},
+ {1370, L"ERROR_RXACT_COMMIT_FAILURE"},
+ {1371, L"ERROR_SPECIAL_ACCOUNT"},
+ {1372, L"ERROR_SPECIAL_GROUP"},
+ {1373, L"ERROR_SPECIAL_USER"},
+ {1374, L"ERROR_MEMBERS_PRIMARY_GROUP"},
+ {1375, L"ERROR_TOKEN_ALREADY_IN_USE"},
+ {1376, L"ERROR_NO_SUCH_ALIAS"},
+ {1377, L"ERROR_MEMBER_NOT_IN_ALIAS"},
+ {1378, L"ERROR_MEMBER_IN_ALIAS"},
+ {1379, L"ERROR_ALIAS_EXISTS"},
+ {1380, L"ERROR_LOGON_NOT_GRANTED"},
+ {1381, L"ERROR_TOO_MANY_SECRETS"},
+ {1382, L"ERROR_SECRET_TOO_LONG"},
+ {1383, L"ERROR_INTERNAL_DB_ERROR"},
+ {1384, L"ERROR_TOO_MANY_CONTEXT_IDS"},
+ {1385, L"ERROR_LOGON_TYPE_NOT_GRANTED"},
+ {1386, L"ERROR_NT_CROSS_ENCRYPTION_REQUIRED"},
+ {1387, L"ERROR_NO_SUCH_MEMBER"},
+ {1388, L"ERROR_INVALID_MEMBER"},
+ {1389, L"ERROR_TOO_MANY_SIDS"},
+ {1390, L"ERROR_LM_CROSS_ENCRYPTION_REQUIRED"},
+ {1391, L"ERROR_NO_INHERITANCE"},
+ {1392, L"ERROR_FILE_CORRUPT"},
+ {1393, L"ERROR_DISK_CORRUPT"},
+ {1394, L"ERROR_NO_USER_SESSION_KEY"},
+ {1395, L"ERROR_LICENSE_QUOTA_EXCEEDED"},
+ {1396, L"ERROR_WRONG_TARGET_NAME"},
+ {1397, L"ERROR_MUTUAL_AUTH_FAILED"},
+ {1398, L"ERROR_TIME_SKEW"},
+ {1399, L"ERROR_CURRENT_DOMAIN_NOT_ALLOWED"},
+ {1400, L"ERROR_INVALID_WINDOW_HANDLE"},
+ {1401, L"ERROR_INVALID_MENU_HANDLE"},
+ {1402, L"ERROR_INVALID_CURSOR_HANDLE"},
+ {1403, L"ERROR_INVALID_ACCEL_HANDLE"},
+ {1404, L"ERROR_INVALID_HOOK_HANDLE"},
+ {1405, L"ERROR_INVALID_DWP_HANDLE"},
+ {1406, L"ERROR_TLW_WITH_WSCHILD"},
+ {1407, L"ERROR_CANNOT_FIND_WND_CLASS"},
+ {1408, L"ERROR_WINDOW_OF_OTHER_THREAD"},
+ {1409, L"ERROR_HOTKEY_ALREADY_REGISTERED"},
+ {1410, L"ERROR_CLASS_ALREADY_EXISTS"},
+ {1411, L"ERROR_CLASS_DOES_NOT_EXIST"},
+ {1412, L"ERROR_CLASS_HAS_WINDOWS"},
+ {1413, L"ERROR_INVALID_INDEX"},
+ {1414, L"ERROR_INVALID_ICON_HANDLE"},
+ {1415, L"ERROR_PRIVATE_DIALOG_INDEX"},
+ {1416, L"ERROR_LISTBOX_ID_NOT_FOUND"},
+ {1417, L"ERROR_NO_WILDCARD_CHARACTERS"},
+ {1418, L"ERROR_CLIPBOARD_NOT_OPEN"},
+ {1419, L"ERROR_HOTKEY_NOT_REGISTERED"},
+ {1420, L"ERROR_WINDOW_NOT_DIALOG"},
+ {1421, L"ERROR_CONTROL_ID_NOT_FOUND"},
+ {1422, L"ERROR_INVALID_COMBOBOX_MESSAGE"},
+ {1423, L"ERROR_WINDOW_NOT_COMBOBOX"},
+ {1424, L"ERROR_INVALID_EDIT_HEIGHT"},
+ {1425, L"ERROR_DC_NOT_FOUND"},
+ {1426, L"ERROR_INVALID_HOOK_FILTER"},
+ {1427, L"ERROR_INVALID_FILTER_PROC"},
+ {1428, L"ERROR_HOOK_NEEDS_HMOD"},
+ {1429, L"ERROR_GLOBAL_ONLY_HOOK"},
+ {1430, L"ERROR_JOURNAL_HOOK_SET"},
+ {1431, L"ERROR_HOOK_NOT_INSTALLED"},
+ {1432, L"ERROR_INVALID_LB_MESSAGE"},
+ {1433, L"ERROR_SETCOUNT_ON_BAD_LB"},
+ {1434, L"ERROR_LB_WITHOUT_TABSTOPS"},
+ {1435, L"ERROR_DESTROY_OBJECT_OF_OTHER_THREAD"},
+ {1436, L"ERROR_CHILD_WINDOW_MENU"},
+ {1437, L"ERROR_NO_SYSTEM_MENU"},
+ {1438, L"ERROR_INVALID_MSGBOX_STYLE"},
+ {1439, L"ERROR_INVALID_SPI_VALUE"},
+ {1440, L"ERROR_SCREEN_ALREADY_LOCKED"},
+ {1441, L"ERROR_HWNDS_HAVE_DIFF_PARENT"},
+ {1442, L"ERROR_NOT_CHILD_WINDOW"},
+ {1443, L"ERROR_INVALID_GW_COMMAND"},
+ {1444, L"ERROR_INVALID_THREAD_ID"},
+ {1445, L"ERROR_NON_MDICHILD_WINDOW"},
+ {1446, L"ERROR_POPUP_ALREADY_ACTIVE"},
+ {1447, L"ERROR_NO_SCROLLBARS"},
+ {1448, L"ERROR_INVALID_SCROLLBAR_RANGE"},
+ {1449, L"ERROR_INVALID_SHOWWIN_COMMAND"},
+ {1450, L"ERROR_NO_SYSTEM_RESOURCES"},
+ {1451, L"ERROR_NONPAGED_SYSTEM_RESOURCES"},
+ {1452, L"ERROR_PAGED_SYSTEM_RESOURCES"},
+ {1453, L"ERROR_WORKING_SET_QUOTA"},
+ {1454, L"ERROR_PAGEFILE_QUOTA"},
+ {1455, L"ERROR_COMMITMENT_LIMIT"},
+ {1456, L"ERROR_MENU_ITEM_NOT_FOUND"},
+ {1457, L"ERROR_INVALID_KEYBOARD_HANDLE"},
+ {1458, L"ERROR_HOOK_TYPE_NOT_ALLOWED"},
+ {1459, L"ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION"},
+ {1460, L"ERROR_TIMEOUT"},
+ {1461, L"ERROR_INVALID_MONITOR_HANDLE"},
+ {1462, L"ERROR_INCORRECT_SIZE"},
+ {1463, L"ERROR_SYMLINK_CLASS_DISABLED"},
+ {1464, L"ERROR_SYMLINK_NOT_SUPPORTED"},
+ {1465, L"ERROR_XML_PARSE_ERROR"},
+ {1466, L"ERROR_XMLDSIG_ERROR"},
+ {1467, L"ERROR_RESTART_APPLICATION"},
+ {1468, L"ERROR_WRONG_COMPARTMENT"},
+ {1469, L"ERROR_AUTHIP_FAILURE"},
+ {1470, L"ERROR_NO_NVRAM_RESOURCES"},
+ {1471, L"ERROR_NOT_GUI_PROCESS"},
+ {1500, L"ERROR_EVENTLOG_FILE_CORRUPT"},
+ {1501, L"ERROR_EVENTLOG_CANT_START"},
+ {1502, L"ERROR_LOG_FILE_FULL"},
+ {1503, L"ERROR_EVENTLOG_FILE_CHANGED"},
+ {1550, L"ERROR_INVALID_TASK_NAME"},
+ {1551, L"ERROR_INVALID_TASK_INDEX"},
+ {1552, L"ERROR_THREAD_ALREADY_IN_TASK"},
+ {1601, L"ERROR_INSTALL_SERVICE_FAILURE"},
+ {1602, L"ERROR_INSTALL_USEREXIT"},
+ {1603, L"ERROR_INSTALL_FAILURE"},
+ {1604, L"ERROR_INSTALL_SUSPEND"},
+ {1605, L"ERROR_UNKNOWN_PRODUCT"},
+ {1606, L"ERROR_UNKNOWN_FEATURE"},
+ {1607, L"ERROR_UNKNOWN_COMPONENT"},
+ {1608, L"ERROR_UNKNOWN_PROPERTY"},
+ {1609, L"ERROR_INVALID_HANDLE_STATE"},
+ {1610, L"ERROR_BAD_CONFIGURATION"},
+ {1611, L"ERROR_INDEX_ABSENT"},
+ {1612, L"ERROR_INSTALL_SOURCE_ABSENT"},
+ {1613, L"ERROR_INSTALL_PACKAGE_VERSION"},
+ {1614, L"ERROR_PRODUCT_UNINSTALLED"},
+ {1615, L"ERROR_BAD_QUERY_SYNTAX"},
+ {1616, L"ERROR_INVALID_FIELD"},
+ {1617, L"ERROR_DEVICE_REMOVED"},
+ {1618, L"ERROR_INSTALL_ALREADY_RUNNING"},
+ {1619, L"ERROR_INSTALL_PACKAGE_OPEN_FAILED"},
+ {1620, L"ERROR_INSTALL_PACKAGE_INVALID"},
+ {1621, L"ERROR_INSTALL_UI_FAILURE"},
+ {1622, L"ERROR_INSTALL_LOG_FAILURE"},
+ {1623, L"ERROR_INSTALL_LANGUAGE_UNSUPPORTED"},
+ {1624, L"ERROR_INSTALL_TRANSFORM_FAILURE"},
+ {1625, L"ERROR_INSTALL_PACKAGE_REJECTED"},
+ {1626, L"ERROR_FUNCTION_NOT_CALLED"},
+ {1627, L"ERROR_FUNCTION_FAILED"},
+ {1628, L"ERROR_INVALID_TABLE"},
+ {1629, L"ERROR_DATATYPE_MISMATCH"},
+ {1630, L"ERROR_UNSUPPORTED_TYPE"},
+ {1631, L"ERROR_CREATE_FAILED"},
+ {1632, L"ERROR_INSTALL_TEMP_UNWRITABLE"},
+ {1633, L"ERROR_INSTALL_PLATFORM_UNSUPPORTED"},
+ {1634, L"ERROR_INSTALL_NOTUSED"},
+ {1635, L"ERROR_PATCH_PACKAGE_OPEN_FAILED"},
+ {1636, L"ERROR_PATCH_PACKAGE_INVALID"},
+ {1637, L"ERROR_PATCH_PACKAGE_UNSUPPORTED"},
+ {1638, L"ERROR_PRODUCT_VERSION"},
+ {1639, L"ERROR_INVALID_COMMAND_LINE"},
+ {1640, L"ERROR_INSTALL_REMOTE_DISALLOWED"},
+ {1641, L"ERROR_SUCCESS_REBOOT_INITIATED"},
+ {1642, L"ERROR_PATCH_TARGET_NOT_FOUND"},
+ {1643, L"ERROR_PATCH_PACKAGE_REJECTED"},
+ {1644, L"ERROR_INSTALL_TRANSFORM_REJECTED"},
+ {1645, L"ERROR_INSTALL_REMOTE_PROHIBITED"},
+ {1646, L"ERROR_PATCH_REMOVAL_UNSUPPORTED"},
+ {1647, L"ERROR_UNKNOWN_PATCH"},
+ {1648, L"ERROR_PATCH_NO_SEQUENCE"},
+ {1649, L"ERROR_PATCH_REMOVAL_DISALLOWED"},
+ {1650, L"ERROR_INVALID_PATCH_XML"},
+ {1651, L"ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT"},
+ {1652, L"ERROR_INSTALL_SERVICE_SAFEBOOT"},
+ {1653, L"ERROR_FAIL_FAST_EXCEPTION"},
+ {1654, L"ERROR_INSTALL_REJECTED"},
+ {1700, L"RPC_S_INVALID_STRING_BINDING"},
+ {1701, L"RPC_S_WRONG_KIND_OF_BINDING"},
+ {1702, L"RPC_S_INVALID_BINDING"},
+ {1703, L"RPC_S_PROTSEQ_NOT_SUPPORTED"},
+ {1704, L"RPC_S_INVALID_RPC_PROTSEQ"},
+ {1705, L"RPC_S_INVALID_STRING_UUID"},
+ {1706, L"RPC_S_INVALID_ENDPOINT_FORMAT"},
+ {1707, L"RPC_S_INVALID_NET_ADDR"},
+ {1708, L"RPC_S_NO_ENDPOINT_FOUND"},
+ {1709, L"RPC_S_INVALID_TIMEOUT"},
+ {1710, L"RPC_S_OBJECT_NOT_FOUND"},
+ {1711, L"RPC_S_ALREADY_REGISTERED"},
+ {1712, L"RPC_S_TYPE_ALREADY_REGISTERED"},
+ {1713, L"RPC_S_ALREADY_LISTENING"},
+ {1714, L"RPC_S_NO_PROTSEQS_REGISTERED"},
+ {1715, L"RPC_S_NOT_LISTENING"},
+ {1716, L"RPC_S_UNKNOWN_MGR_TYPE"},
+ {1717, L"RPC_S_UNKNOWN_IF"},
+ {1718, L"RPC_S_NO_BINDINGS"},
+ {1719, L"RPC_S_NO_PROTSEQS"},
+ {1720, L"RPC_S_CANT_CREATE_ENDPOINT"},
+ {1721, L"RPC_S_OUT_OF_RESOURCES"},
+ {1722, L"RPC_S_SERVER_UNAVAILABLE"},
+ {1723, L"RPC_S_SERVER_TOO_BUSY"},
+ {1724, L"RPC_S_INVALID_NETWORK_OPTIONS"},
+ {1725, L"RPC_S_NO_CALL_ACTIVE"},
+ {1726, L"RPC_S_CALL_FAILED"},
+ {1727, L"RPC_S_CALL_FAILED_DNE"},
+ {1728, L"RPC_S_PROTOCOL_ERROR"},
+ {1729, L"RPC_S_PROXY_ACCESS_DENIED"},
+ {1730, L"RPC_S_UNSUPPORTED_TRANS_SYN"},
+ {1732, L"RPC_S_UNSUPPORTED_TYPE"},
+ {1733, L"RPC_S_INVALID_TAG"},
+ {1734, L"RPC_S_INVALID_BOUND"},
+ {1735, L"RPC_S_NO_ENTRY_NAME"},
+ {1736, L"RPC_S_INVALID_NAME_SYNTAX"},
+ {1737, L"RPC_S_UNSUPPORTED_NAME_SYNTAX"},
+ {1739, L"RPC_S_UUID_NO_ADDRESS"},
+ {1740, L"RPC_S_DUPLICATE_ENDPOINT"},
+ {1741, L"RPC_S_UNKNOWN_AUTHN_TYPE"},
+ {1742, L"RPC_S_MAX_CALLS_TOO_SMALL"},
+ {1743, L"RPC_S_STRING_TOO_LONG"},
+ {1744, L"RPC_S_PROTSEQ_NOT_FOUND"},
+ {1745, L"RPC_S_PROCNUM_OUT_OF_RANGE"},
+ {1746, L"RPC_S_BINDING_HAS_NO_AUTH"},
+ {1747, L"RPC_S_UNKNOWN_AUTHN_SERVICE"},
+ {1748, L"RPC_S_UNKNOWN_AUTHN_LEVEL"},
+ {1749, L"RPC_S_INVALID_AUTH_IDENTITY"},
+ {1750, L"RPC_S_UNKNOWN_AUTHZ_SERVICE"},
+ {1751, L"EPT_S_INVALID_ENTRY"},
+ {1752, L"EPT_S_CANT_PERFORM_OP"},
+ {1753, L"EPT_S_NOT_REGISTERED"},
+ {1754, L"RPC_S_NOTHING_TO_EXPORT"},
+ {1755, L"RPC_S_INCOMPLETE_NAME"},
+ {1756, L"RPC_S_INVALID_VERS_OPTION"},
+ {1757, L"RPC_S_NO_MORE_MEMBERS"},
+ {1758, L"RPC_S_NOT_ALL_OBJS_UNEXPORTED"},
+ {1759, L"RPC_S_INTERFACE_NOT_FOUND"},
+ {1760, L"RPC_S_ENTRY_ALREADY_EXISTS"},
+ {1761, L"RPC_S_ENTRY_NOT_FOUND"},
+ {1762, L"RPC_S_NAME_SERVICE_UNAVAILABLE"},
+ {1763, L"RPC_S_INVALID_NAF_ID"},
+ {1764, L"RPC_S_CANNOT_SUPPORT"},
+ {1765, L"RPC_S_NO_CONTEXT_AVAILABLE"},
+ {1766, L"RPC_S_INTERNAL_ERROR"},
+ {1767, L"RPC_S_ZERO_DIVIDE"},
+ {1768, L"RPC_S_ADDRESS_ERROR"},
+ {1769, L"RPC_S_FP_DIV_ZERO"},
+ {1770, L"RPC_S_FP_UNDERFLOW"},
+ {1771, L"RPC_S_FP_OVERFLOW"},
+ {1772, L"RPC_X_NO_MORE_ENTRIES"},
+ {1773, L"RPC_X_SS_CHAR_TRANS_OPEN_FAIL"},
+ {1774, L"RPC_X_SS_CHAR_TRANS_SHORT_FILE"},
+ {1775, L"RPC_X_SS_IN_NULL_CONTEXT"},
+ {1777, L"RPC_X_SS_CONTEXT_DAMAGED"},
+ {1778, L"RPC_X_SS_HANDLES_MISMATCH"},
+ {1779, L"RPC_X_SS_CANNOT_GET_CALL_HANDLE"},
+ {1780, L"RPC_X_NULL_REF_POINTER"},
+ {1781, L"RPC_X_ENUM_VALUE_OUT_OF_RANGE"},
+ {1782, L"RPC_X_BYTE_COUNT_TOO_SMALL"},
+ {1783, L"RPC_X_BAD_STUB_DATA"},
+ {1784, L"ERROR_INVALID_USER_BUFFER"},
+ {1785, L"ERROR_UNRECOGNIZED_MEDIA"},
+ {1786, L"ERROR_NO_TRUST_LSA_SECRET"},
+ {1787, L"ERROR_NO_TRUST_SAM_ACCOUNT"},
+ {1788, L"ERROR_TRUSTED_DOMAIN_FAILURE"},
+ {1789, L"ERROR_TRUSTED_RELATIONSHIP_FAILURE"},
+ {1790, L"ERROR_TRUST_FAILURE"},
+ {1791, L"RPC_S_CALL_IN_PROGRESS"},
+ {1792, L"ERROR_NETLOGON_NOT_STARTED"},
+ {1793, L"ERROR_ACCOUNT_EXPIRED"},
+ {1794, L"ERROR_REDIRECTOR_HAS_OPEN_HANDLES"},
+ {1795, L"ERROR_PRINTER_DRIVER_ALREADY_INSTALLED"},
+ {1796, L"ERROR_UNKNOWN_PORT"},
+ {1797, L"ERROR_UNKNOWN_PRINTER_DRIVER"},
+ {1798, L"ERROR_UNKNOWN_PRINTPROCESSOR"},
+ {1799, L"ERROR_INVALID_SEPARATOR_FILE"},
+ {1800, L"ERROR_INVALID_PRIORITY"},
+ {1801, L"ERROR_INVALID_PRINTER_NAME"},
+ {1802, L"ERROR_PRINTER_ALREADY_EXISTS"},
+ {1803, L"ERROR_INVALID_PRINTER_COMMAND"},
+ {1804, L"ERROR_INVALID_DATATYPE"},
+ {1805, L"ERROR_INVALID_ENVIRONMENT"},
+ {1806, L"RPC_S_NO_MORE_BINDINGS"},
+ {1807, L"ERROR_NOLOGON_INTERDOMAIN_TRUST_ACCOUNT"},
+ {1808, L"ERROR_NOLOGON_WORKSTATION_TRUST_ACCOUNT"},
+ {1809, L"ERROR_NOLOGON_SERVER_TRUST_ACCOUNT"},
+ {1810, L"ERROR_DOMAIN_TRUST_INCONSISTENT"},
+ {1811, L"ERROR_SERVER_HAS_OPEN_HANDLES"},
+ {1812, L"ERROR_RESOURCE_DATA_NOT_FOUND"},
+ {1813, L"ERROR_RESOURCE_TYPE_NOT_FOUND"},
+ {1814, L"ERROR_RESOURCE_NAME_NOT_FOUND"},
+ {1815, L"ERROR_RESOURCE_LANG_NOT_FOUND"},
+ {1816, L"ERROR_NOT_ENOUGH_QUOTA"},
+ {1817, L"RPC_S_NO_INTERFACES"},
+ {1818, L"RPC_S_CALL_CANCELLED"},
+ {1819, L"RPC_S_BINDING_INCOMPLETE"},
+ {1820, L"RPC_S_COMM_FAILURE"},
+ {1821, L"RPC_S_UNSUPPORTED_AUTHN_LEVEL"},
+ {1822, L"RPC_S_NO_PRINC_NAME"},
+ {1823, L"RPC_S_NOT_RPC_ERROR"},
+ {1824, L"RPC_S_UUID_LOCAL_ONLY"},
+ {1825, L"RPC_S_SEC_PKG_ERROR"},
+ {1826, L"RPC_S_NOT_CANCELLED"},
+ {1827, L"RPC_X_INVALID_ES_ACTION"},
+ {1828, L"RPC_X_WRONG_ES_VERSION"},
+ {1829, L"RPC_X_WRONG_STUB_VERSION"},
+ {1830, L"RPC_X_INVALID_PIPE_OBJECT"},
+ {1831, L"RPC_X_WRONG_PIPE_ORDER"},
+ {1832, L"RPC_X_WRONG_PIPE_VERSION"},
+ {1833, L"RPC_S_COOKIE_AUTH_FAILED"},
+ {1898, L"RPC_S_GROUP_MEMBER_NOT_FOUND"},
+ {1899, L"EPT_S_CANT_CREATE"},
+ {1900, L"RPC_S_INVALID_OBJECT"},
+ {1901, L"ERROR_INVALID_TIME"},
+ {1902, L"ERROR_INVALID_FORM_NAME"},
+ {1903, L"ERROR_INVALID_FORM_SIZE"},
+ {1904, L"ERROR_ALREADY_WAITING"},
+ {1905, L"ERROR_PRINTER_DELETED"},
+ {1906, L"ERROR_INVALID_PRINTER_STATE"},
+ {1907, L"ERROR_PASSWORD_MUST_CHANGE"},
+ {1908, L"ERROR_DOMAIN_CONTROLLER_NOT_FOUND"},
+ {1909, L"ERROR_ACCOUNT_LOCKED_OUT"},
+ {1910, L"OR_INVALID_OXID"},
+ {1911, L"OR_INVALID_OID"},
+ {1912, L"OR_INVALID_SET"},
+ {1913, L"RPC_S_SEND_INCOMPLETE"},
+ {1914, L"RPC_S_INVALID_ASYNC_HANDLE"},
+ {1915, L"RPC_S_INVALID_ASYNC_CALL"},
+ {1916, L"RPC_X_PIPE_CLOSED"},
+ {1917, L"RPC_X_PIPE_DISCIPLINE_ERROR"},
+ {1918, L"RPC_X_PIPE_EMPTY"},
+ {1919, L"ERROR_NO_SITENAME"},
+ {1920, L"ERROR_CANT_ACCESS_FILE"},
+ {1921, L"ERROR_CANT_RESOLVE_FILENAME"},
+ {1922, L"RPC_S_ENTRY_TYPE_MISMATCH"},
+ {1923, L"RPC_S_NOT_ALL_OBJS_EXPORTED"},
+ {1924, L"RPC_S_INTERFACE_NOT_EXPORTED"},
+ {1925, L"RPC_S_PROFILE_NOT_ADDED"},
+ {1926, L"RPC_S_PRF_ELT_NOT_ADDED"},
+ {1927, L"RPC_S_PRF_ELT_NOT_REMOVED"},
+ {1928, L"RPC_S_GRP_ELT_NOT_ADDED"},
+ {1929, L"RPC_S_GRP_ELT_NOT_REMOVED"},
+ {1930, L"ERROR_KM_DRIVER_BLOCKED"},
+ {1931, L"ERROR_CONTEXT_EXPIRED"},
+ {1932, L"ERROR_PER_USER_TRUST_QUOTA_EXCEEDED"},
+ {1933, L"ERROR_ALL_USER_TRUST_QUOTA_EXCEEDED"},
+ {1934, L"ERROR_USER_DELETE_TRUST_QUOTA_EXCEEDED"},
+ {1935, L"ERROR_AUTHENTICATION_FIREWALL_FAILED"},
+ {1936, L"ERROR_REMOTE_PRINT_CONNECTIONS_BLOCKED"},
+ {1937, L"ERROR_NTLM_BLOCKED"},
+ {1938, L"ERROR_PASSWORD_CHANGE_REQUIRED"},
+ {2000, L"ERROR_INVALID_PIXEL_FORMAT"},
+ {2001, L"ERROR_BAD_DRIVER"},
+ {2002, L"ERROR_INVALID_WINDOW_STYLE"},
+ {2003, L"ERROR_METAFILE_NOT_SUPPORTED"},
+ {2004, L"ERROR_TRANSFORM_NOT_SUPPORTED"},
+ {2005, L"ERROR_CLIPPING_NOT_SUPPORTED"},
+ {2010, L"ERROR_INVALID_CMM"},
+ {2011, L"ERROR_INVALID_PROFILE"},
+ {2012, L"ERROR_TAG_NOT_FOUND"},
+ {2013, L"ERROR_TAG_NOT_PRESENT"},
+ {2014, L"ERROR_DUPLICATE_TAG"},
+ {2015, L"ERROR_PROFILE_NOT_ASSOCIATED_WITH_DEVICE"},
+ {2016, L"ERROR_PROFILE_NOT_FOUND"},
+ {2017, L"ERROR_INVALID_COLORSPACE"},
+ {2018, L"ERROR_ICM_NOT_ENABLED"},
+ {2019, L"ERROR_DELETING_ICM_XFORM"},
+ {2020, L"ERROR_INVALID_TRANSFORM"},
+ {2021, L"ERROR_COLORSPACE_MISMATCH"},
+ {2022, L"ERROR_INVALID_COLORINDEX"},
+ {2023, L"ERROR_PROFILE_DOES_NOT_MATCH_DEVICE"},
+ {2108, L"ERROR_CONNECTED_OTHER_PASSWORD"},
+ {2109, L"ERROR_CONNECTED_OTHER_PASSWORD_DEFAULT"},
+ {2202, L"ERROR_BAD_USERNAME"},
+ {2250, L"ERROR_NOT_CONNECTED"},
+ {2401, L"ERROR_OPEN_FILES"},
+ {2402, L"ERROR_ACTIVE_CONNECTIONS"},
+ {2404, L"ERROR_DEVICE_IN_USE"},
+ {3000, L"ERROR_UNKNOWN_PRINT_MONITOR"},
+ {3001, L"ERROR_PRINTER_DRIVER_IN_USE"},
+ {3002, L"ERROR_SPOOL_FILE_NOT_FOUND"},
+ {3003, L"ERROR_SPL_NO_STARTDOC"},
+ {3004, L"ERROR_SPL_NO_ADDJOB"},
+ {3005, L"ERROR_PRINT_PROCESSOR_ALREADY_INSTALLED"},
+ {3006, L"ERROR_PRINT_MONITOR_ALREADY_INSTALLED"},
+ {3007, L"ERROR_INVALID_PRINT_MONITOR"},
+ {3008, L"ERROR_PRINT_MONITOR_IN_USE"},
+ {3009, L"ERROR_PRINTER_HAS_JOBS_QUEUED"},
+ {3010, L"ERROR_SUCCESS_REBOOT_REQUIRED"},
+ {3011, L"ERROR_SUCCESS_RESTART_REQUIRED"},
+ {3012, L"ERROR_PRINTER_NOT_FOUND"},
+ {3013, L"ERROR_PRINTER_DRIVER_WARNED"},
+ {3014, L"ERROR_PRINTER_DRIVER_BLOCKED"},
+ {3015, L"ERROR_PRINTER_DRIVER_PACKAGE_IN_USE"},
+ {3016, L"ERROR_CORE_DRIVER_PACKAGE_NOT_FOUND"},
+ {3017, L"ERROR_FAIL_REBOOT_REQUIRED"},
+ {3018, L"ERROR_FAIL_REBOOT_INITIATED"},
+ {3019, L"ERROR_PRINTER_DRIVER_DOWNLOAD_NEEDED"},
+ {3020, L"ERROR_PRINT_JOB_RESTART_REQUIRED"},
+ {3021, L"ERROR_INVALID_PRINTER_DRIVER_MANIFEST"},
+ {3022, L"ERROR_PRINTER_NOT_SHAREABLE"},
+ {3050, L"ERROR_REQUEST_PAUSED"},
+ {3950, L"ERROR_IO_REISSUE_AS_CACHED"},
+ {4000, L"ERROR_WINS_INTERNAL"},
+ {4001, L"ERROR_CAN_NOT_DEL_LOCAL_WINS"},
+ {4002, L"ERROR_STATIC_INIT"},
+ {4003, L"ERROR_INC_BACKUP"},
+ {4004, L"ERROR_FULL_BACKUP"},
+ {4005, L"ERROR_REC_NON_EXISTENT"},
+ {4006, L"ERROR_RPL_NOT_ALLOWED"},
+ {4050, L"PEERDIST_ERROR_CONTENTINFO_VERSION_UNSUPPORTED"},
+ {4051, L"PEERDIST_ERROR_CANNOT_PARSE_CONTENTINFO"},
+ {4052, L"PEERDIST_ERROR_MISSING_DATA"},
+ {4053, L"PEERDIST_ERROR_NO_MORE"},
+ {4054, L"PEERDIST_ERROR_NOT_INITIALIZED"},
+ {4055, L"PEERDIST_ERROR_ALREADY_INITIALIZED"},
+ {4056, L"PEERDIST_ERROR_SHUTDOWN_IN_PROGRESS"},
+ {4057, L"PEERDIST_ERROR_INVALIDATED"},
+ {4058, L"PEERDIST_ERROR_ALREADY_EXISTS"},
+ {4059, L"PEERDIST_ERROR_OPERATION_NOTFOUND"},
+ {4060, L"PEERDIST_ERROR_ALREADY_COMPLETED"},
+ {4061, L"PEERDIST_ERROR_OUT_OF_BOUNDS"},
+ {4062, L"PEERDIST_ERROR_VERSION_UNSUPPORTED"},
+ {4063, L"PEERDIST_ERROR_INVALID_CONFIGURATION"},
+ {4064, L"PEERDIST_ERROR_NOT_LICENSED"},
+ {4065, L"PEERDIST_ERROR_SERVICE_UNAVAILABLE"},
+ {4066, L"PEERDIST_ERROR_TRUST_FAILURE"},
+ {4100, L"ERROR_DHCP_ADDRESS_CONFLICT"},
+ {4200, L"ERROR_WMI_GUID_NOT_FOUND"},
+ {4201, L"ERROR_WMI_INSTANCE_NOT_FOUND"},
+ {4202, L"ERROR_WMI_ITEMID_NOT_FOUND"},
+ {4203, L"ERROR_WMI_TRY_AGAIN"},
+ {4204, L"ERROR_WMI_DP_NOT_FOUND"},
+ {4205, L"ERROR_WMI_UNRESOLVED_INSTANCE_REF"},
+ {4206, L"ERROR_WMI_ALREADY_ENABLED"},
+ {4207, L"ERROR_WMI_GUID_DISCONNECTED"},
+ {4208, L"ERROR_WMI_SERVER_UNAVAILABLE"},
+ {4209, L"ERROR_WMI_DP_FAILED"},
+ {4210, L"ERROR_WMI_INVALID_MOF"},
+ {4211, L"ERROR_WMI_INVALID_REGINFO"},
+ {4212, L"ERROR_WMI_ALREADY_DISABLED"},
+ {4213, L"ERROR_WMI_READ_ONLY"},
+ {4214, L"ERROR_WMI_SET_FAILURE"},
+ {4250, L"ERROR_NOT_APPCONTAINER"},
+ {4251, L"ERROR_APPCONTAINER_REQUIRED"},
+ {4252, L"ERROR_NOT_SUPPORTED_IN_APPCONTAINER"},
+ {4253, L"ERROR_INVALID_PACKAGE_SID_LENGTH"},
+ {4300, L"ERROR_INVALID_MEDIA"},
+ {4301, L"ERROR_INVALID_LIBRARY"},
+ {4302, L"ERROR_INVALID_MEDIA_POOL"},
+ {4303, L"ERROR_DRIVE_MEDIA_MISMATCH"},
+ {4304, L"ERROR_MEDIA_OFFLINE"},
+ {4305, L"ERROR_LIBRARY_OFFLINE"},
+ {4306, L"ERROR_EMPTY"},
+ {4307, L"ERROR_NOT_EMPTY"},
+ {4308, L"ERROR_MEDIA_UNAVAILABLE"},
+ {4309, L"ERROR_RESOURCE_DISABLED"},
+ {4310, L"ERROR_INVALID_CLEANER"},
+ {4311, L"ERROR_UNABLE_TO_CLEAN"},
+ {4312, L"ERROR_OBJECT_NOT_FOUND"},
+ {4313, L"ERROR_DATABASE_FAILURE"},
+ {4314, L"ERROR_DATABASE_FULL"},
+ {4315, L"ERROR_MEDIA_INCOMPATIBLE"},
+ {4316, L"ERROR_RESOURCE_NOT_PRESENT"},
+ {4317, L"ERROR_INVALID_OPERATION"},
+ {4318, L"ERROR_MEDIA_NOT_AVAILABLE"},
+ {4319, L"ERROR_DEVICE_NOT_AVAILABLE"},
+ {4320, L"ERROR_REQUEST_REFUSED"},
+ {4321, L"ERROR_INVALID_DRIVE_OBJECT"},
+ {4322, L"ERROR_LIBRARY_FULL"},
+ {4323, L"ERROR_MEDIUM_NOT_ACCESSIBLE"},
+ {4324, L"ERROR_UNABLE_TO_LOAD_MEDIUM"},
+ {4325, L"ERROR_UNABLE_TO_INVENTORY_DRIVE"},
+ {4326, L"ERROR_UNABLE_TO_INVENTORY_SLOT"},
+ {4327, L"ERROR_UNABLE_TO_INVENTORY_TRANSPORT"},
+ {4328, L"ERROR_TRANSPORT_FULL"},
+ {4329, L"ERROR_CONTROLLING_IEPORT"},
+ {4330, L"ERROR_UNABLE_TO_EJECT_MOUNTED_MEDIA"},
+ {4331, L"ERROR_CLEANER_SLOT_SET"},
+ {4332, L"ERROR_CLEANER_SLOT_NOT_SET"},
+ {4333, L"ERROR_CLEANER_CARTRIDGE_SPENT"},
+ {4334, L"ERROR_UNEXPECTED_OMID"},
+ {4335, L"ERROR_CANT_DELETE_LAST_ITEM"},
+ {4336, L"ERROR_MESSAGE_EXCEEDS_MAX_SIZE"},
+ {4337, L"ERROR_VOLUME_CONTAINS_SYS_FILES"},
+ {4338, L"ERROR_INDIGENOUS_TYPE"},
+ {4339, L"ERROR_NO_SUPPORTING_DRIVES"},
+ {4340, L"ERROR_CLEANER_CARTRIDGE_INSTALLED"},
+ {4341, L"ERROR_IEPORT_FULL"},
+ {4350, L"ERROR_FILE_OFFLINE"},
+ {4351, L"ERROR_REMOTE_STORAGE_NOT_ACTIVE"},
+ {4352, L"ERROR_REMOTE_STORAGE_MEDIA_ERROR"},
+ {4390, L"ERROR_NOT_A_REPARSE_POINT"},
+ {4391, L"ERROR_REPARSE_ATTRIBUTE_CONFLICT"},
+ {4392, L"ERROR_INVALID_REPARSE_DATA"},
+ {4393, L"ERROR_REPARSE_TAG_INVALID"},
+ {4394, L"ERROR_REPARSE_TAG_MISMATCH"},
+ {4400, L"ERROR_APP_DATA_NOT_FOUND"},
+ {4401, L"ERROR_APP_DATA_EXPIRED"},
+ {4402, L"ERROR_APP_DATA_CORRUPT"},
+ {4403, L"ERROR_APP_DATA_LIMIT_EXCEEDED"},
+ {4404, L"ERROR_APP_DATA_REBOOT_REQUIRED"},
+ {4420, L"ERROR_SECUREBOOT_ROLLBACK_DETECTED"},
+ {4421, L"ERROR_SECUREBOOT_POLICY_VIOLATION"},
+ {4422, L"ERROR_SECUREBOOT_INVALID_POLICY"},
+ {4423, L"ERROR_SECUREBOOT_POLICY_PUBLISHER_NOT_FOUND"},
+ {4424, L"ERROR_SECUREBOOT_POLICY_NOT_SIGNED"},
+ {4425, L"ERROR_SECUREBOOT_NOT_ENABLED"},
+ {4426, L"ERROR_SECUREBOOT_FILE_REPLACED"},
+ {4440, L"ERROR_OFFLOAD_READ_FLT_NOT_SUPPORTED"},
+ {4441, L"ERROR_OFFLOAD_WRITE_FLT_NOT_SUPPORTED"},
+ {4442, L"ERROR_OFFLOAD_READ_FILE_NOT_SUPPORTED"},
+ {4443, L"ERROR_OFFLOAD_WRITE_FILE_NOT_SUPPORTED"},
+ {4500, L"ERROR_VOLUME_NOT_SIS_ENABLED"},
+ {5001, L"ERROR_DEPENDENT_RESOURCE_EXISTS"},
+ {5002, L"ERROR_DEPENDENCY_NOT_FOUND"},
+ {5003, L"ERROR_DEPENDENCY_ALREADY_EXISTS"},
+ {5004, L"ERROR_RESOURCE_NOT_ONLINE"},
+ {5005, L"ERROR_HOST_NODE_NOT_AVAILABLE"},
+ {5006, L"ERROR_RESOURCE_NOT_AVAILABLE"},
+ {5007, L"ERROR_RESOURCE_NOT_FOUND"},
+ {5008, L"ERROR_SHUTDOWN_CLUSTER"},
+ {5009, L"ERROR_CANT_EVICT_ACTIVE_NODE"},
+ {5010, L"ERROR_OBJECT_ALREADY_EXISTS"},
+ {5011, L"ERROR_OBJECT_IN_LIST"},
+ {5012, L"ERROR_GROUP_NOT_AVAILABLE"},
+ {5013, L"ERROR_GROUP_NOT_FOUND"},
+ {5014, L"ERROR_GROUP_NOT_ONLINE"},
+ {5015, L"ERROR_HOST_NODE_NOT_RESOURCE_OWNER"},
+ {5016, L"ERROR_HOST_NODE_NOT_GROUP_OWNER"},
+ {5017, L"ERROR_RESMON_CREATE_FAILED"},
+ {5018, L"ERROR_RESMON_ONLINE_FAILED"},
+ {5019, L"ERROR_RESOURCE_ONLINE"},
+ {5020, L"ERROR_QUORUM_RESOURCE"},
+ {5021, L"ERROR_NOT_QUORUM_CAPABLE"},
+ {5022, L"ERROR_CLUSTER_SHUTTING_DOWN"},
+ {5023, L"ERROR_INVALID_STATE"},
+ {5024, L"ERROR_RESOURCE_PROPERTIES_STORED"},
+ {5025, L"ERROR_NOT_QUORUM_CLASS"},
+ {5026, L"ERROR_CORE_RESOURCE"},
+ {5027, L"ERROR_QUORUM_RESOURCE_ONLINE_FAILED"},
+ {5028, L"ERROR_QUORUMLOG_OPEN_FAILED"},
+ {5029, L"ERROR_CLUSTERLOG_CORRUPT"},
+ {5030, L"ERROR_CLUSTERLOG_RECORD_EXCEEDS_MAXSIZE"},
+ {5031, L"ERROR_CLUSTERLOG_EXCEEDS_MAXSIZE"},
+ {5032, L"ERROR_CLUSTERLOG_CHKPOINT_NOT_FOUND"},
+ {5033, L"ERROR_CLUSTERLOG_NOT_ENOUGH_SPACE"},
+ {5034, L"ERROR_QUORUM_OWNER_ALIVE"},
+ {5035, L"ERROR_NETWORK_NOT_AVAILABLE"},
+ {5036, L"ERROR_NODE_NOT_AVAILABLE"},
+ {5037, L"ERROR_ALL_NODES_NOT_AVAILABLE"},
+ {5038, L"ERROR_RESOURCE_FAILED"},
+ {5039, L"ERROR_CLUSTER_INVALID_NODE"},
+ {5040, L"ERROR_CLUSTER_NODE_EXISTS"},
+ {5041, L"ERROR_CLUSTER_JOIN_IN_PROGRESS"},
+ {5042, L"ERROR_CLUSTER_NODE_NOT_FOUND"},
+ {5043, L"ERROR_CLUSTER_LOCAL_NODE_NOT_FOUND"},
+ {5044, L"ERROR_CLUSTER_NETWORK_EXISTS"},
+ {5045, L"ERROR_CLUSTER_NETWORK_NOT_FOUND"},
+ {5046, L"ERROR_CLUSTER_NETINTERFACE_EXISTS"},
+ {5047, L"ERROR_CLUSTER_NETINTERFACE_NOT_FOUND"},
+ {5048, L"ERROR_CLUSTER_INVALID_REQUEST"},
+ {5049, L"ERROR_CLUSTER_INVALID_NETWORK_PROVIDER"},
+ {5050, L"ERROR_CLUSTER_NODE_DOWN"},
+ {5051, L"ERROR_CLUSTER_NODE_UNREACHABLE"},
+ {5052, L"ERROR_CLUSTER_NODE_NOT_MEMBER"},
+ {5053, L"ERROR_CLUSTER_JOIN_NOT_IN_PROGRESS"},
+ {5054, L"ERROR_CLUSTER_INVALID_NETWORK"},
+ {5056, L"ERROR_CLUSTER_NODE_UP"},
+ {5057, L"ERROR_CLUSTER_IPADDR_IN_USE"},
+ {5058, L"ERROR_CLUSTER_NODE_NOT_PAUSED"},
+ {5059, L"ERROR_CLUSTER_NO_SECURITY_CONTEXT"},
+ {5060, L"ERROR_CLUSTER_NETWORK_NOT_INTERNAL"},
+ {5061, L"ERROR_CLUSTER_NODE_ALREADY_UP"},
+ {5062, L"ERROR_CLUSTER_NODE_ALREADY_DOWN"},
+ {5063, L"ERROR_CLUSTER_NETWORK_ALREADY_ONLINE"},
+ {5064, L"ERROR_CLUSTER_NETWORK_ALREADY_OFFLINE"},
+ {5065, L"ERROR_CLUSTER_NODE_ALREADY_MEMBER"},
+ {5066, L"ERROR_CLUSTER_LAST_INTERNAL_NETWORK"},
+ {5067, L"ERROR_CLUSTER_NETWORK_HAS_DEPENDENTS"},
+ {5068, L"ERROR_INVALID_OPERATION_ON_QUORUM"},
+ {5069, L"ERROR_DEPENDENCY_NOT_ALLOWED"},
+ {5070, L"ERROR_CLUSTER_NODE_PAUSED"},
+ {5071, L"ERROR_NODE_CANT_HOST_RESOURCE"},
+ {5072, L"ERROR_CLUSTER_NODE_NOT_READY"},
+ {5073, L"ERROR_CLUSTER_NODE_SHUTTING_DOWN"},
+ {5074, L"ERROR_CLUSTER_JOIN_ABORTED"},
+ {5075, L"ERROR_CLUSTER_INCOMPATIBLE_VERSIONS"},
+ {5076, L"ERROR_CLUSTER_MAXNUM_OF_RESOURCES_EXCEEDED"},
+ {5077, L"ERROR_CLUSTER_SYSTEM_CONFIG_CHANGED"},
+ {5078, L"ERROR_CLUSTER_RESOURCE_TYPE_NOT_FOUND"},
+ {5079, L"ERROR_CLUSTER_RESTYPE_NOT_SUPPORTED"},
+ {5080, L"ERROR_CLUSTER_RESNAME_NOT_FOUND"},
+ {5081, L"ERROR_CLUSTER_NO_RPC_PACKAGES_REGISTERED"},
+ {5082, L"ERROR_CLUSTER_OWNER_NOT_IN_PREFLIST"},
+ {5083, L"ERROR_CLUSTER_DATABASE_SEQMISMATCH"},
+ {5084, L"ERROR_RESMON_INVALID_STATE"},
+ {5085, L"ERROR_CLUSTER_GUM_NOT_LOCKER"},
+ {5086, L"ERROR_QUORUM_DISK_NOT_FOUND"},
+ {5087, L"ERROR_DATABASE_BACKUP_CORRUPT"},
+ {5088, L"ERROR_CLUSTER_NODE_ALREADY_HAS_DFS_ROOT"},
+ {5089, L"ERROR_RESOURCE_PROPERTY_UNCHANGEABLE"},
+ {5890, L"ERROR_CLUSTER_MEMBERSHIP_INVALID_STATE"},
+ {5891, L"ERROR_CLUSTER_QUORUMLOG_NOT_FOUND"},
+ {5892, L"ERROR_CLUSTER_MEMBERSHIP_HALT"},
+ {5893, L"ERROR_CLUSTER_INSTANCE_ID_MISMATCH"},
+ {5894, L"ERROR_CLUSTER_NETWORK_NOT_FOUND_FOR_IP"},
+ {5895, L"ERROR_CLUSTER_PROPERTY_DATA_TYPE_MISMATCH"},
+ {5896, L"ERROR_CLUSTER_EVICT_WITHOUT_CLEANUP"},
+ {5897, L"ERROR_CLUSTER_PARAMETER_MISMATCH"},
+ {5898, L"ERROR_NODE_CANNOT_BE_CLUSTERED"},
+ {5899, L"ERROR_CLUSTER_WRONG_OS_VERSION"},
+ {5900, L"ERROR_CLUSTER_CANT_CREATE_DUP_CLUSTER_NAME"},
+ {5901, L"ERROR_CLUSCFG_ALREADY_COMMITTED"},
+ {5902, L"ERROR_CLUSCFG_ROLLBACK_FAILED"},
+ {5903, L"ERROR_CLUSCFG_SYSTEM_DISK_DRIVE_LETTER_CONFLICT"},
+ {5904, L"ERROR_CLUSTER_OLD_VERSION"},
+ {5905, L"ERROR_CLUSTER_MISMATCHED_COMPUTER_ACCT_NAME"},
+ {5906, L"ERROR_CLUSTER_NO_NET_ADAPTERS"},
+ {5907, L"ERROR_CLUSTER_POISONED"},
+ {5908, L"ERROR_CLUSTER_GROUP_MOVING"},
+ {5909, L"ERROR_CLUSTER_RESOURCE_TYPE_BUSY"},
+ {5910, L"ERROR_RESOURCE_CALL_TIMED_OUT"},
+ {5911, L"ERROR_INVALID_CLUSTER_IPV6_ADDRESS"},
+ {5912, L"ERROR_CLUSTER_INTERNAL_INVALID_FUNCTION"},
+ {5913, L"ERROR_CLUSTER_PARAMETER_OUT_OF_BOUNDS"},
+ {5914, L"ERROR_CLUSTER_PARTIAL_SEND"},
+ {5915, L"ERROR_CLUSTER_REGISTRY_INVALID_FUNCTION"},
+ {5916, L"ERROR_CLUSTER_INVALID_STRING_TERMINATION"},
+ {5917, L"ERROR_CLUSTER_INVALID_STRING_FORMAT"},
+ {5918, L"ERROR_CLUSTER_DATABASE_TRANSACTION_IN_PROGRESS"},
+ {5919, L"ERROR_CLUSTER_DATABASE_TRANSACTION_NOT_IN_PROGRESS"},
+ {5920, L"ERROR_CLUSTER_NULL_DATA"},
+ {5921, L"ERROR_CLUSTER_PARTIAL_READ"},
+ {5922, L"ERROR_CLUSTER_PARTIAL_WRITE"},
+ {5923, L"ERROR_CLUSTER_CANT_DESERIALIZE_DATA"},
+ {5924, L"ERROR_DEPENDENT_RESOURCE_PROPERTY_CONFLICT"},
+ {5925, L"ERROR_CLUSTER_NO_QUORUM"},
+ {5926, L"ERROR_CLUSTER_INVALID_IPV6_NETWORK"},
+ {5927, L"ERROR_CLUSTER_INVALID_IPV6_TUNNEL_NETWORK"},
+ {5928, L"ERROR_QUORUM_NOT_ALLOWED_IN_THIS_GROUP"},
+ {5929, L"ERROR_DEPENDENCY_TREE_TOO_COMPLEX"},
+ {5930, L"ERROR_EXCEPTION_IN_RESOURCE_CALL"},
+ {5931, L"ERROR_CLUSTER_RHS_FAILED_INITIALIZATION"},
+ {5932, L"ERROR_CLUSTER_NOT_INSTALLED"},
+ {5933, L"ERROR_CLUSTER_RESOURCES_MUST_BE_ONLINE_ON_THE_SAME_NODE"},
+ {5934, L"ERROR_CLUSTER_MAX_NODES_IN_CLUSTER"},
+ {5935, L"ERROR_CLUSTER_TOO_MANY_NODES"},
+ {5936, L"ERROR_CLUSTER_OBJECT_ALREADY_USED"},
+ {5937, L"ERROR_NONCORE_GROUPS_FOUND"},
+ {5938, L"ERROR_FILE_SHARE_RESOURCE_CONFLICT"},
+ {5939, L"ERROR_CLUSTER_EVICT_INVALID_REQUEST"},
+ {5940, L"ERROR_CLUSTER_SINGLETON_RESOURCE"},
+ {5941, L"ERROR_CLUSTER_GROUP_SINGLETON_RESOURCE"},
+ {5942, L"ERROR_CLUSTER_RESOURCE_PROVIDER_FAILED"},
+ {5943, L"ERROR_CLUSTER_RESOURCE_CONFIGURATION_ERROR"},
+ {5944, L"ERROR_CLUSTER_GROUP_BUSY"},
+ {5945, L"ERROR_CLUSTER_NOT_SHARED_VOLUME"},
+ {5946, L"ERROR_CLUSTER_INVALID_SECURITY_DESCRIPTOR"},
+ {5947, L"ERROR_CLUSTER_SHARED_VOLUMES_IN_USE"},
+ {5948, L"ERROR_CLUSTER_USE_SHARED_VOLUMES_API"},
+ {5949, L"ERROR_CLUSTER_BACKUP_IN_PROGRESS"},
+ {5950, L"ERROR_NON_CSV_PATH"},
+ {5951, L"ERROR_CSV_VOLUME_NOT_LOCAL"},
+ {5952, L"ERROR_CLUSTER_WATCHDOG_TERMINATING"},
+ {5953, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_INCOMPATIBLE_NODES"},
+ {5954, L"ERROR_CLUSTER_INVALID_NODE_WEIGHT"},
+ {5955, L"ERROR_CLUSTER_RESOURCE_VETOED_CALL"},
+ {5956, L"ERROR_RESMON_SYSTEM_RESOURCES_LACKING"},
+ {5957, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_NOT_ENOUGH_RESOURCES_ON_DESTINATION"},
+ {5958, L"ERROR_CLUSTER_RESOURCE_VETOED_MOVE_NOT_ENOUGH_RESOURCES_ON_SOURCE"},
+ {5959, L"ERROR_CLUSTER_GROUP_QUEUED"},
+ {5960, L"ERROR_CLUSTER_RESOURCE_LOCKED_STATUS"},
+ {5961, L"ERROR_CLUSTER_SHARED_VOLUME_FAILOVER_NOT_ALLOWED"},
+ {5962, L"ERROR_CLUSTER_NODE_DRAIN_IN_PROGRESS"},
+ {5963, L"ERROR_CLUSTER_DISK_NOT_CONNECTED"},
+ {5964, L"ERROR_DISK_NOT_CSV_CAPABLE"},
+ {5965, L"ERROR_RESOURCE_NOT_IN_AVAILABLE_STORAGE"},
+ {5966, L"ERROR_CLUSTER_SHARED_VOLUME_REDIRECTED"},
+ {5967, L"ERROR_CLUSTER_SHARED_VOLUME_NOT_REDIRECTED"},
+ {5968, L"ERROR_CLUSTER_CANNOT_RETURN_PROPERTIES"},
+ {5969, L"ERROR_CLUSTER_RESOURCE_CONTAINS_UNSUPPORTED_DIFF_AREA_FOR_SHARED_VOLUMES"},
+ {5970, L"ERROR_CLUSTER_RESOURCE_IS_IN_MAINTENANCE_MODE"},
+ {5971, L"ERROR_CLUSTER_AFFINITY_CONFLICT"},
+ {5972, L"ERROR_CLUSTER_RESOURCE_IS_REPLICA_VIRTUAL_MACHINE"},
+ {6000, L"ERROR_ENCRYPTION_FAILED"},
+ {6001, L"ERROR_DECRYPTION_FAILED"},
+ {6002, L"ERROR_FILE_ENCRYPTED"},
+ {6003, L"ERROR_NO_RECOVERY_POLICY"},
+ {6004, L"ERROR_NO_EFS"},
+ {6005, L"ERROR_WRONG_EFS"},
+ {6006, L"ERROR_NO_USER_KEYS"},
+ {6007, L"ERROR_FILE_NOT_ENCRYPTED"},
+ {6008, L"ERROR_NOT_EXPORT_FORMAT"},
+ {6009, L"ERROR_FILE_READ_ONLY"},
+ {6010, L"ERROR_DIR_EFS_DISALLOWED"},
+ {6011, L"ERROR_EFS_SERVER_NOT_TRUSTED"},
+ {6012, L"ERROR_BAD_RECOVERY_POLICY"},
+ {6013, L"ERROR_EFS_ALG_BLOB_TOO_BIG"},
+ {6014, L"ERROR_VOLUME_NOT_SUPPORT_EFS"},
+ {6015, L"ERROR_EFS_DISABLED"},
+ {6016, L"ERROR_EFS_VERSION_NOT_SUPPORT"},
+ {6017, L"ERROR_CS_ENCRYPTION_INVALID_SERVER_RESPONSE"},
+ {6018, L"ERROR_CS_ENCRYPTION_UNSUPPORTED_SERVER"},
+ {6019, L"ERROR_CS_ENCRYPTION_EXISTING_ENCRYPTED_FILE"},
+ {6020, L"ERROR_CS_ENCRYPTION_NEW_ENCRYPTED_FILE"},
+ {6021, L"ERROR_CS_ENCRYPTION_FILE_NOT_CSE"},
+ {6022, L"ERROR_ENCRYPTION_POLICY_DENIES_OPERATION"},
+ {6118, L"ERROR_NO_BROWSER_SERVERS_FOUND"},
+ {6200, L"SCHED_E_SERVICE_NOT_LOCALSYSTEM"},
+ {6600, L"ERROR_LOG_SECTOR_INVALID"},
+ {6601, L"ERROR_LOG_SECTOR_PARITY_INVALID"},
+ {6602, L"ERROR_LOG_SECTOR_REMAPPED"},
+ {6603, L"ERROR_LOG_BLOCK_INCOMPLETE"},
+ {6604, L"ERROR_LOG_INVALID_RANGE"},
+ {6605, L"ERROR_LOG_BLOCKS_EXHAUSTED"},
+ {6606, L"ERROR_LOG_READ_CONTEXT_INVALID"},
+ {6607, L"ERROR_LOG_RESTART_INVALID"},
+ {6608, L"ERROR_LOG_BLOCK_VERSION"},
+ {6609, L"ERROR_LOG_BLOCK_INVALID"},
+ {6610, L"ERROR_LOG_READ_MODE_INVALID"},
+ {6611, L"ERROR_LOG_NO_RESTART"},
+ {6612, L"ERROR_LOG_METADATA_CORRUPT"},
+ {6613, L"ERROR_LOG_METADATA_INVALID"},
+ {6614, L"ERROR_LOG_METADATA_INCONSISTENT"},
+ {6615, L"ERROR_LOG_RESERVATION_INVALID"},
+ {6616, L"ERROR_LOG_CANT_DELETE"},
+ {6617, L"ERROR_LOG_CONTAINER_LIMIT_EXCEEDED"},
+ {6618, L"ERROR_LOG_START_OF_LOG"},
+ {6619, L"ERROR_LOG_POLICY_ALREADY_INSTALLED"},
+ {6620, L"ERROR_LOG_POLICY_NOT_INSTALLED"},
+ {6621, L"ERROR_LOG_POLICY_INVALID"},
+ {6622, L"ERROR_LOG_POLICY_CONFLICT"},
+ {6623, L"ERROR_LOG_PINNED_ARCHIVE_TAIL"},
+ {6624, L"ERROR_LOG_RECORD_NONEXISTENT"},
+ {6625, L"ERROR_LOG_RECORDS_RESERVED_INVALID"},
+ {6626, L"ERROR_LOG_SPACE_RESERVED_INVALID"},
+ {6627, L"ERROR_LOG_TAIL_INVALID"},
+ {6628, L"ERROR_LOG_FULL"},
+ {6629, L"ERROR_COULD_NOT_RESIZE_LOG"},
+ {6630, L"ERROR_LOG_MULTIPLEXED"},
+ {6631, L"ERROR_LOG_DEDICATED"},
+ {6632, L"ERROR_LOG_ARCHIVE_NOT_IN_PROGRESS"},
+ {6633, L"ERROR_LOG_ARCHIVE_IN_PROGRESS"},
+ {6634, L"ERROR_LOG_EPHEMERAL"},
+ {6635, L"ERROR_LOG_NOT_ENOUGH_CONTAINERS"},
+ {6636, L"ERROR_LOG_CLIENT_ALREADY_REGISTERED"},
+ {6637, L"ERROR_LOG_CLIENT_NOT_REGISTERED"},
+ {6638, L"ERROR_LOG_FULL_HANDLER_IN_PROGRESS"},
+ {6639, L"ERROR_LOG_CONTAINER_READ_FAILED"},
+ {6640, L"ERROR_LOG_CONTAINER_WRITE_FAILED"},
+ {6641, L"ERROR_LOG_CONTAINER_OPEN_FAILED"},
+ {6642, L"ERROR_LOG_CONTAINER_STATE_INVALID"},
+ {6643, L"ERROR_LOG_STATE_INVALID"},
+ {6644, L"ERROR_LOG_PINNED"},
+ {6645, L"ERROR_LOG_METADATA_FLUSH_FAILED"},
+ {6646, L"ERROR_LOG_INCONSISTENT_SECURITY"},
+ {6647, L"ERROR_LOG_APPENDED_FLUSH_FAILED"},
+ {6648, L"ERROR_LOG_PINNED_RESERVATION"},
+ {6700, L"ERROR_INVALID_TRANSACTION"},
+ {6701, L"ERROR_TRANSACTION_NOT_ACTIVE"},
+ {6702, L"ERROR_TRANSACTION_REQUEST_NOT_VALID"},
+ {6703, L"ERROR_TRANSACTION_NOT_REQUESTED"},
+ {6704, L"ERROR_TRANSACTION_ALREADY_ABORTED"},
+ {6705, L"ERROR_TRANSACTION_ALREADY_COMMITTED"},
+ {6706, L"ERROR_TM_INITIALIZATION_FAILED"},
+ {6707, L"ERROR_RESOURCEMANAGER_READ_ONLY"},
+ {6708, L"ERROR_TRANSACTION_NOT_JOINED"},
+ {6709, L"ERROR_TRANSACTION_SUPERIOR_EXISTS"},
+ {6710, L"ERROR_CRM_PROTOCOL_ALREADY_EXISTS"},
+ {6711, L"ERROR_TRANSACTION_PROPAGATION_FAILED"},
+ {6712, L"ERROR_CRM_PROTOCOL_NOT_FOUND"},
+ {6713, L"ERROR_TRANSACTION_INVALID_MARSHALL_BUFFER"},
+ {6714, L"ERROR_CURRENT_TRANSACTION_NOT_VALID"},
+ {6715, L"ERROR_TRANSACTION_NOT_FOUND"},
+ {6716, L"ERROR_RESOURCEMANAGER_NOT_FOUND"},
+ {6717, L"ERROR_ENLISTMENT_NOT_FOUND"},
+ {6718, L"ERROR_TRANSACTIONMANAGER_NOT_FOUND"},
+ {6719, L"ERROR_TRANSACTIONMANAGER_NOT_ONLINE"},
+ {6720, L"ERROR_TRANSACTIONMANAGER_RECOVERY_NAME_COLLISION"},
+ {6721, L"ERROR_TRANSACTION_NOT_ROOT"},
+ {6722, L"ERROR_TRANSACTION_OBJECT_EXPIRED"},
+ {6723, L"ERROR_TRANSACTION_RESPONSE_NOT_ENLISTED"},
+ {6724, L"ERROR_TRANSACTION_RECORD_TOO_LONG"},
+ {6725, L"ERROR_IMPLICIT_TRANSACTION_NOT_SUPPORTED"},
+ {6726, L"ERROR_TRANSACTION_INTEGRITY_VIOLATED"},
+ {6727, L"ERROR_TRANSACTIONMANAGER_IDENTITY_MISMATCH"},
+ {6728, L"ERROR_RM_CANNOT_BE_FROZEN_FOR_SNAPSHOT"},
+ {6729, L"ERROR_TRANSACTION_MUST_WRITETHROUGH"},
+ {6730, L"ERROR_TRANSACTION_NO_SUPERIOR"},
+ {6731, L"ERROR_HEURISTIC_DAMAGE_POSSIBLE"},
+ {6800, L"ERROR_TRANSACTIONAL_CONFLICT"},
+ {6801, L"ERROR_RM_NOT_ACTIVE"},
+ {6802, L"ERROR_RM_METADATA_CORRUPT"},
+ {6803, L"ERROR_DIRECTORY_NOT_RM"},
+ {6805, L"ERROR_TRANSACTIONS_UNSUPPORTED_REMOTE"},
+ {6806, L"ERROR_LOG_RESIZE_INVALID_SIZE"},
+ {6807, L"ERROR_OBJECT_NO_LONGER_EXISTS"},
+ {6808, L"ERROR_STREAM_MINIVERSION_NOT_FOUND"},
+ {6809, L"ERROR_STREAM_MINIVERSION_NOT_VALID"},
+ {6810, L"ERROR_MINIVERSION_INACCESSIBLE_FROM_SPECIFIED_TRANSACTION"},
+ {6811, L"ERROR_CANT_OPEN_MINIVERSION_WITH_MODIFY_INTENT"},
+ {6812, L"ERROR_CANT_CREATE_MORE_STREAM_MINIVERSIONS"},
+ {6814, L"ERROR_REMOTE_FILE_VERSION_MISMATCH"},
+ {6815, L"ERROR_HANDLE_NO_LONGER_VALID"},
+ {6816, L"ERROR_NO_TXF_METADATA"},
+ {6817, L"ERROR_LOG_CORRUPTION_DETECTED"},
+ {6818, L"ERROR_CANT_RECOVER_WITH_HANDLE_OPEN"},
+ {6819, L"ERROR_RM_DISCONNECTED"},
+ {6820, L"ERROR_ENLISTMENT_NOT_SUPERIOR"},
+ {6821, L"ERROR_RECOVERY_NOT_NEEDED"},
+ {6822, L"ERROR_RM_ALREADY_STARTED"},
+ {6823, L"ERROR_FILE_IDENTITY_NOT_PERSISTENT"},
+ {6824, L"ERROR_CANT_BREAK_TRANSACTIONAL_DEPENDENCY"},
+ {6825, L"ERROR_CANT_CROSS_RM_BOUNDARY"},
+ {6826, L"ERROR_TXF_DIR_NOT_EMPTY"},
+ {6827, L"ERROR_INDOUBT_TRANSACTIONS_EXIST"},
+ {6828, L"ERROR_TM_VOLATILE"},
+ {6829, L"ERROR_ROLLBACK_TIMER_EXPIRED"},
+ {6830, L"ERROR_TXF_ATTRIBUTE_CORRUPT"},
+ {6831, L"ERROR_EFS_NOT_ALLOWED_IN_TRANSACTION"},
+ {6832, L"ERROR_TRANSACTIONAL_OPEN_NOT_ALLOWED"},
+ {6833, L"ERROR_LOG_GROWTH_FAILED"},
+ {6834, L"ERROR_TRANSACTED_MAPPING_UNSUPPORTED_REMOTE"},
+ {6835, L"ERROR_TXF_METADATA_ALREADY_PRESENT"},
+ {6836, L"ERROR_TRANSACTION_SCOPE_CALLBACKS_NOT_SET"},
+ {6837, L"ERROR_TRANSACTION_REQUIRED_PROMOTION"},
+ {6838, L"ERROR_CANNOT_EXECUTE_FILE_IN_TRANSACTION"},
+ {6839, L"ERROR_TRANSACTIONS_NOT_FROZEN"},
+ {6840, L"ERROR_TRANSACTION_FREEZE_IN_PROGRESS"},
+ {6841, L"ERROR_NOT_SNAPSHOT_VOLUME"},
+ {6842, L"ERROR_NO_SAVEPOINT_WITH_OPEN_FILES"},
+ {6843, L"ERROR_DATA_LOST_REPAIR"},
+ {6844, L"ERROR_SPARSE_NOT_ALLOWED_IN_TRANSACTION"},
+ {6845, L"ERROR_TM_IDENTITY_MISMATCH"},
+ {6846, L"ERROR_FLOATED_SECTION"},
+ {6847, L"ERROR_CANNOT_ACCEPT_TRANSACTED_WORK"},
+ {6848, L"ERROR_CANNOT_ABORT_TRANSACTIONS"},
+ {6849, L"ERROR_BAD_CLUSTERS"},
+ {6850, L"ERROR_COMPRESSION_NOT_ALLOWED_IN_TRANSACTION"},
+ {6851, L"ERROR_VOLUME_DIRTY"},
+ {6852, L"ERROR_NO_LINK_TRACKING_IN_TRANSACTION"},
+ {6853, L"ERROR_OPERATION_NOT_SUPPORTED_IN_TRANSACTION"},
+ {6854, L"ERROR_EXPIRED_HANDLE"},
+ {6855, L"ERROR_TRANSACTION_NOT_ENLISTED"},
+ {7001, L"ERROR_CTX_WINSTATION_NAME_INVALID"},
+ {7002, L"ERROR_CTX_INVALID_PD"},
+ {7003, L"ERROR_CTX_PD_NOT_FOUND"},
+ {7004, L"ERROR_CTX_WD_NOT_FOUND"},
+ {7005, L"ERROR_CTX_CANNOT_MAKE_EVENTLOG_ENTRY"},
+ {7006, L"ERROR_CTX_SERVICE_NAME_COLLISION"},
+ {7007, L"ERROR_CTX_CLOSE_PENDING"},
+ {7008, L"ERROR_CTX_NO_OUTBUF"},
+ {7009, L"ERROR_CTX_MODEM_INF_NOT_FOUND"},
+ {7010, L"ERROR_CTX_INVALID_MODEMNAME"},
+ {7011, L"ERROR_CTX_MODEM_RESPONSE_ERROR"},
+ {7012, L"ERROR_CTX_MODEM_RESPONSE_TIMEOUT"},
+ {7013, L"ERROR_CTX_MODEM_RESPONSE_NO_CARRIER"},
+ {7014, L"ERROR_CTX_MODEM_RESPONSE_NO_DIALTONE"},
+ {7015, L"ERROR_CTX_MODEM_RESPONSE_BUSY"},
+ {7016, L"ERROR_CTX_MODEM_RESPONSE_VOICE"},
+ {7017, L"ERROR_CTX_TD_ERROR"},
+ {7022, L"ERROR_CTX_WINSTATION_NOT_FOUND"},
+ {7023, L"ERROR_CTX_WINSTATION_ALREADY_EXISTS"},
+ {7024, L"ERROR_CTX_WINSTATION_BUSY"},
+ {7025, L"ERROR_CTX_BAD_VIDEO_MODE"},
+ {7035, L"ERROR_CTX_GRAPHICS_INVALID"},
+ {7037, L"ERROR_CTX_LOGON_DISABLED"},
+ {7038, L"ERROR_CTX_NOT_CONSOLE"},
+ {7040, L"ERROR_CTX_CLIENT_QUERY_TIMEOUT"},
+ {7041, L"ERROR_CTX_CONSOLE_DISCONNECT"},
+ {7042, L"ERROR_CTX_CONSOLE_CONNECT"},
+ {7044, L"ERROR_CTX_SHADOW_DENIED"},
+ {7045, L"ERROR_CTX_WINSTATION_ACCESS_DENIED"},
+ {7049, L"ERROR_CTX_INVALID_WD"},
+ {7050, L"ERROR_CTX_SHADOW_INVALID"},
+ {7051, L"ERROR_CTX_SHADOW_DISABLED"},
+ {7052, L"ERROR_CTX_CLIENT_LICENSE_IN_USE"},
+ {7053, L"ERROR_CTX_CLIENT_LICENSE_NOT_SET"},
+ {7054, L"ERROR_CTX_LICENSE_NOT_AVAILABLE"},
+ {7055, L"ERROR_CTX_LICENSE_CLIENT_INVALID"},
+ {7056, L"ERROR_CTX_LICENSE_EXPIRED"},
+ {7057, L"ERROR_CTX_SHADOW_NOT_RUNNING"},
+ {7058, L"ERROR_CTX_SHADOW_ENDED_BY_MODE_CHANGE"},
+ {7059, L"ERROR_ACTIVATION_COUNT_EXCEEDED"},
+ {7060, L"ERROR_CTX_WINSTATIONS_DISABLED"},
+ {7061, L"ERROR_CTX_ENCRYPTION_LEVEL_REQUIRED"},
+ {7062, L"ERROR_CTX_SESSION_IN_USE"},
+ {7063, L"ERROR_CTX_NO_FORCE_LOGOFF"},
+ {7064, L"ERROR_CTX_ACCOUNT_RESTRICTION"},
+ {7065, L"ERROR_RDP_PROTOCOL_ERROR"},
+ {7066, L"ERROR_CTX_CDM_CONNECT"},
+ {7067, L"ERROR_CTX_CDM_DISCONNECT"},
+ {7068, L"ERROR_CTX_SECURITY_LAYER_ERROR"},
+ {7069, L"ERROR_TS_INCOMPATIBLE_SESSIONS"},
+ {7070, L"ERROR_TS_VIDEO_SUBSYSTEM_ERROR"},
+ {8001, L"FRS_ERR_INVALID_API_SEQUENCE"},
+ {8002, L"FRS_ERR_STARTING_SERVICE"},
+ {8003, L"FRS_ERR_STOPPING_SERVICE"},
+ {8004, L"FRS_ERR_INTERNAL_API"},
+ {8005, L"FRS_ERR_INTERNAL"},
+ {8006, L"FRS_ERR_SERVICE_COMM"},
+ {8007, L"FRS_ERR_INSUFFICIENT_PRIV"},
+ {8008, L"FRS_ERR_AUTHENTICATION"},
+ {8009, L"FRS_ERR_PARENT_INSUFFICIENT_PRIV"},
+ {8010, L"FRS_ERR_PARENT_AUTHENTICATION"},
+ {8011, L"FRS_ERR_CHILD_TO_PARENT_COMM"},
+ {8012, L"FRS_ERR_PARENT_TO_CHILD_COMM"},
+ {8013, L"FRS_ERR_SYSVOL_POPULATE"},
+ {8014, L"FRS_ERR_SYSVOL_POPULATE_TIMEOUT"},
+ {8015, L"FRS_ERR_SYSVOL_IS_BUSY"},
+ {8016, L"FRS_ERR_SYSVOL_DEMOTE"},
+ {8017, L"FRS_ERR_INVALID_SERVICE_PARAMETER"},
+ {8200, L"ERROR_DS_NOT_INSTALLED"},
+ {8201, L"ERROR_DS_MEMBERSHIP_EVALUATED_LOCALLY"},
+ {8202, L"ERROR_DS_NO_ATTRIBUTE_OR_VALUE"},
+ {8203, L"ERROR_DS_INVALID_ATTRIBUTE_SYNTAX"},
+ {8204, L"ERROR_DS_ATTRIBUTE_TYPE_UNDEFINED"},
+ {8205, L"ERROR_DS_ATTRIBUTE_OR_VALUE_EXISTS"},
+ {8206, L"ERROR_DS_BUSY"},
+ {8207, L"ERROR_DS_UNAVAILABLE"},
+ {8208, L"ERROR_DS_NO_RIDS_ALLOCATED"},
+ {8209, L"ERROR_DS_NO_MORE_RIDS"},
+ {8210, L"ERROR_DS_INCORRECT_ROLE_OWNER"},
+ {8211, L"ERROR_DS_RIDMGR_INIT_ERROR"},
+ {8212, L"ERROR_DS_OBJ_CLASS_VIOLATION"},
+ {8213, L"ERROR_DS_CANT_ON_NON_LEAF"},
+ {8214, L"ERROR_DS_CANT_ON_RDN"},
+ {8215, L"ERROR_DS_CANT_MOD_OBJ_CLASS"},
+ {8216, L"ERROR_DS_CROSS_DOM_MOVE_ERROR"},
+ {8217, L"ERROR_DS_GC_NOT_AVAILABLE"},
+ {8218, L"ERROR_SHARED_POLICY"},
+ {8219, L"ERROR_POLICY_OBJECT_NOT_FOUND"},
+ {8220, L"ERROR_POLICY_ONLY_IN_DS"},
+ {8221, L"ERROR_PROMOTION_ACTIVE"},
+ {8222, L"ERROR_NO_PROMOTION_ACTIVE"},
+ {8224, L"ERROR_DS_OPERATIONS_ERROR"},
+ {8225, L"ERROR_DS_PROTOCOL_ERROR"},
+ {8226, L"ERROR_DS_TIMELIMIT_EXCEEDED"},
+ {8227, L"ERROR_DS_SIZELIMIT_EXCEEDED"},
+ {8228, L"ERROR_DS_ADMIN_LIMIT_EXCEEDED"},
+ {8229, L"ERROR_DS_COMPARE_FALSE"},
+ {8230, L"ERROR_DS_COMPARE_TRUE"},
+ {8231, L"ERROR_DS_AUTH_METHOD_NOT_SUPPORTED"},
+ {8232, L"ERROR_DS_STRONG_AUTH_REQUIRED"},
+ {8233, L"ERROR_DS_INAPPROPRIATE_AUTH"},
+ {8234, L"ERROR_DS_AUTH_UNKNOWN"},
+ {8235, L"ERROR_DS_REFERRAL"},
+ {8236, L"ERROR_DS_UNAVAILABLE_CRIT_EXTENSION"},
+ {8237, L"ERROR_DS_CONFIDENTIALITY_REQUIRED"},
+ {8238, L"ERROR_DS_INAPPROPRIATE_MATCHING"},
+ {8239, L"ERROR_DS_CONSTRAINT_VIOLATION"},
+ {8240, L"ERROR_DS_NO_SUCH_OBJECT"},
+ {8241, L"ERROR_DS_ALIAS_PROBLEM"},
+ {8242, L"ERROR_DS_INVALID_DN_SYNTAX"},
+ {8243, L"ERROR_DS_IS_LEAF"},
+ {8244, L"ERROR_DS_ALIAS_DEREF_PROBLEM"},
+ {8245, L"ERROR_DS_UNWILLING_TO_PERFORM"},
+ {8246, L"ERROR_DS_LOOP_DETECT"},
+ {8247, L"ERROR_DS_NAMING_VIOLATION"},
+ {8248, L"ERROR_DS_OBJECT_RESULTS_TOO_LARGE"},
+ {8249, L"ERROR_DS_AFFECTS_MULTIPLE_DSAS"},
+ {8250, L"ERROR_DS_SERVER_DOWN"},
+ {8251, L"ERROR_DS_LOCAL_ERROR"},
+ {8252, L"ERROR_DS_ENCODING_ERROR"},
+ {8253, L"ERROR_DS_DECODING_ERROR"},
+ {8254, L"ERROR_DS_FILTER_UNKNOWN"},
+ {8255, L"ERROR_DS_PARAM_ERROR"},
+ {8256, L"ERROR_DS_NOT_SUPPORTED"},
+ {8257, L"ERROR_DS_NO_RESULTS_RETURNED"},
+ {8258, L"ERROR_DS_CONTROL_NOT_FOUND"},
+ {8259, L"ERROR_DS_CLIENT_LOOP"},
+ {8260, L"ERROR_DS_REFERRAL_LIMIT_EXCEEDED"},
+ {8261, L"ERROR_DS_SORT_CONTROL_MISSING"},
+ {8262, L"ERROR_DS_OFFSET_RANGE_ERROR"},
+ {8263, L"ERROR_DS_RIDMGR_DISABLED"},
+ {8301, L"ERROR_DS_ROOT_MUST_BE_NC"},
+ {8302, L"ERROR_DS_ADD_REPLICA_INHIBITED"},
+ {8303, L"ERROR_DS_ATT_NOT_DEF_IN_SCHEMA"},
+ {8304, L"ERROR_DS_MAX_OBJ_SIZE_EXCEEDED"},
+ {8305, L"ERROR_DS_OBJ_STRING_NAME_EXISTS"},
+ {8306, L"ERROR_DS_NO_RDN_DEFINED_IN_SCHEMA"},
+ {8307, L"ERROR_DS_RDN_DOESNT_MATCH_SCHEMA"},
+ {8308, L"ERROR_DS_NO_REQUESTED_ATTS_FOUND"},
+ {8309, L"ERROR_DS_USER_BUFFER_TO_SMALL"},
+ {8310, L"ERROR_DS_ATT_IS_NOT_ON_OBJ"},
+ {8311, L"ERROR_DS_ILLEGAL_MOD_OPERATION"},
+ {8312, L"ERROR_DS_OBJ_TOO_LARGE"},
+ {8313, L"ERROR_DS_BAD_INSTANCE_TYPE"},
+ {8314, L"ERROR_DS_MASTERDSA_REQUIRED"},
+ {8315, L"ERROR_DS_OBJECT_CLASS_REQUIRED"},
+ {8316, L"ERROR_DS_MISSING_REQUIRED_ATT"},
+ {8317, L"ERROR_DS_ATT_NOT_DEF_FOR_CLASS"},
+ {8318, L"ERROR_DS_ATT_ALREADY_EXISTS"},
+ {8320, L"ERROR_DS_CANT_ADD_ATT_VALUES"},
+ {8321, L"ERROR_DS_SINGLE_VALUE_CONSTRAINT"},
+ {8322, L"ERROR_DS_RANGE_CONSTRAINT"},
+ {8323, L"ERROR_DS_ATT_VAL_ALREADY_EXISTS"},
+ {8324, L"ERROR_DS_CANT_REM_MISSING_ATT"},
+ {8325, L"ERROR_DS_CANT_REM_MISSING_ATT_VAL"},
+ {8326, L"ERROR_DS_ROOT_CANT_BE_SUBREF"},
+ {8327, L"ERROR_DS_NO_CHAINING"},
+ {8328, L"ERROR_DS_NO_CHAINED_EVAL"},
+ {8329, L"ERROR_DS_NO_PARENT_OBJECT"},
+ {8330, L"ERROR_DS_PARENT_IS_AN_ALIAS"},
+ {8331, L"ERROR_DS_CANT_MIX_MASTER_AND_REPS"},
+ {8332, L"ERROR_DS_CHILDREN_EXIST"},
+ {8333, L"ERROR_DS_OBJ_NOT_FOUND"},
+ {8334, L"ERROR_DS_ALIASED_OBJ_MISSING"},
+ {8335, L"ERROR_DS_BAD_NAME_SYNTAX"},
+ {8336, L"ERROR_DS_ALIAS_POINTS_TO_ALIAS"},
+ {8337, L"ERROR_DS_CANT_DEREF_ALIAS"},
+ {8338, L"ERROR_DS_OUT_OF_SCOPE"},
+ {8339, L"ERROR_DS_OBJECT_BEING_REMOVED"},
+ {8340, L"ERROR_DS_CANT_DELETE_DSA_OBJ"},
+ {8341, L"ERROR_DS_GENERIC_ERROR"},
+ {8342, L"ERROR_DS_DSA_MUST_BE_INT_MASTER"},
+ {8343, L"ERROR_DS_CLASS_NOT_DSA"},
+ {8344, L"ERROR_DS_INSUFF_ACCESS_RIGHTS"},
+ {8345, L"ERROR_DS_ILLEGAL_SUPERIOR"},
+ {8346, L"ERROR_DS_ATTRIBUTE_OWNED_BY_SAM"},
+ {8347, L"ERROR_DS_NAME_TOO_MANY_PARTS"},
+ {8348, L"ERROR_DS_NAME_TOO_LONG"},
+ {8349, L"ERROR_DS_NAME_VALUE_TOO_LONG"},
+ {8350, L"ERROR_DS_NAME_UNPARSEABLE"},
+ {8351, L"ERROR_DS_NAME_TYPE_UNKNOWN"},
+ {8352, L"ERROR_DS_NOT_AN_OBJECT"},
+ {8353, L"ERROR_DS_SEC_DESC_TOO_SHORT"},
+ {8354, L"ERROR_DS_SEC_DESC_INVALID"},
+ {8355, L"ERROR_DS_NO_DELETED_NAME"},
+ {8356, L"ERROR_DS_SUBREF_MUST_HAVE_PARENT"},
+ {8357, L"ERROR_DS_NCNAME_MUST_BE_NC"},
+ {8358, L"ERROR_DS_CANT_ADD_SYSTEM_ONLY"},
+ {8359, L"ERROR_DS_CLASS_MUST_BE_CONCRETE"},
+ {8360, L"ERROR_DS_INVALID_DMD"},
+ {8361, L"ERROR_DS_OBJ_GUID_EXISTS"},
+ {8362, L"ERROR_DS_NOT_ON_BACKLINK"},
+ {8363, L"ERROR_DS_NO_CROSSREF_FOR_NC"},
+ {8364, L"ERROR_DS_SHUTTING_DOWN"},
+ {8365, L"ERROR_DS_UNKNOWN_OPERATION"},
+ {8366, L"ERROR_DS_INVALID_ROLE_OWNER"},
+ {8367, L"ERROR_DS_COULDNT_CONTACT_FSMO"},
+ {8368, L"ERROR_DS_CROSS_NC_DN_RENAME"},
+ {8369, L"ERROR_DS_CANT_MOD_SYSTEM_ONLY"},
+ {8370, L"ERROR_DS_REPLICATOR_ONLY"},
+ {8371, L"ERROR_DS_OBJ_CLASS_NOT_DEFINED"},
+ {8372, L"ERROR_DS_OBJ_CLASS_NOT_SUBCLASS"},
+ {8373, L"ERROR_DS_NAME_REFERENCE_INVALID"},
+ {8374, L"ERROR_DS_CROSS_REF_EXISTS"},
+ {8375, L"ERROR_DS_CANT_DEL_MASTER_CROSSREF"},
+ {8376, L"ERROR_DS_SUBTREE_NOTIFY_NOT_NC_HEAD"},
+ {8377, L"ERROR_DS_NOTIFY_FILTER_TOO_COMPLEX"},
+ {8378, L"ERROR_DS_DUP_RDN"},
+ {8379, L"ERROR_DS_DUP_OID"},
+ {8380, L"ERROR_DS_DUP_MAPI_ID"},
+ {8381, L"ERROR_DS_DUP_SCHEMA_ID_GUID"},
+ {8382, L"ERROR_DS_DUP_LDAP_DISPLAY_NAME"},
+ {8383, L"ERROR_DS_SEMANTIC_ATT_TEST"},
+ {8384, L"ERROR_DS_SYNTAX_MISMATCH"},
+ {8385, L"ERROR_DS_EXISTS_IN_MUST_HAVE"},
+ {8386, L"ERROR_DS_EXISTS_IN_MAY_HAVE"},
+ {8387, L"ERROR_DS_NONEXISTENT_MAY_HAVE"},
+ {8388, L"ERROR_DS_NONEXISTENT_MUST_HAVE"},
+ {8389, L"ERROR_DS_AUX_CLS_TEST_FAIL"},
+ {8390, L"ERROR_DS_NONEXISTENT_POSS_SUP"},
+ {8391, L"ERROR_DS_SUB_CLS_TEST_FAIL"},
+ {8392, L"ERROR_DS_BAD_RDN_ATT_ID_SYNTAX"},
+ {8393, L"ERROR_DS_EXISTS_IN_AUX_CLS"},
+ {8394, L"ERROR_DS_EXISTS_IN_SUB_CLS"},
+ {8395, L"ERROR_DS_EXISTS_IN_POSS_SUP"},
+ {8396, L"ERROR_DS_RECALCSCHEMA_FAILED"},
+ {8397, L"ERROR_DS_TREE_DELETE_NOT_FINISHED"},
+ {8398, L"ERROR_DS_CANT_DELETE"},
+ {8399, L"ERROR_DS_ATT_SCHEMA_REQ_ID"},
+ {8400, L"ERROR_DS_BAD_ATT_SCHEMA_SYNTAX"},
+ {8401, L"ERROR_DS_CANT_CACHE_ATT"},
+ {8402, L"ERROR_DS_CANT_CACHE_CLASS"},
+ {8403, L"ERROR_DS_CANT_REMOVE_ATT_CACHE"},
+ {8404, L"ERROR_DS_CANT_REMOVE_CLASS_CACHE"},
+ {8405, L"ERROR_DS_CANT_RETRIEVE_DN"},
+ {8406, L"ERROR_DS_MISSING_SUPREF"},
+ {8407, L"ERROR_DS_CANT_RETRIEVE_INSTANCE"},
+ {8408, L"ERROR_DS_CODE_INCONSISTENCY"},
+ {8409, L"ERROR_DS_DATABASE_ERROR"},
+ {8410, L"ERROR_DS_GOVERNSID_MISSING"},
+ {8411, L"ERROR_DS_MISSING_EXPECTED_ATT"},
+ {8412, L"ERROR_DS_NCNAME_MISSING_CR_REF"},
+ {8413, L"ERROR_DS_SECURITY_CHECKING_ERROR"},
+ {8414, L"ERROR_DS_SCHEMA_NOT_LOADED"},
+ {8415, L"ERROR_DS_SCHEMA_ALLOC_FAILED"},
+ {8416, L"ERROR_DS_ATT_SCHEMA_REQ_SYNTAX"},
+ {8417, L"ERROR_DS_GCVERIFY_ERROR"},
+ {8418, L"ERROR_DS_DRA_SCHEMA_MISMATCH"},
+ {8419, L"ERROR_DS_CANT_FIND_DSA_OBJ"},
+ {8420, L"ERROR_DS_CANT_FIND_EXPECTED_NC"},
+ {8421, L"ERROR_DS_CANT_FIND_NC_IN_CACHE"},
+ {8422, L"ERROR_DS_CANT_RETRIEVE_CHILD"},
+ {8423, L"ERROR_DS_SECURITY_ILLEGAL_MODIFY"},
+ {8424, L"ERROR_DS_CANT_REPLACE_HIDDEN_REC"},
+ {8425, L"ERROR_DS_BAD_HIERARCHY_FILE"},
+ {8426, L"ERROR_DS_BUILD_HIERARCHY_TABLE_FAILED"},
+ {8427, L"ERROR_DS_CONFIG_PARAM_MISSING"},
+ {8428, L"ERROR_DS_COUNTING_AB_INDICES_FAILED"},
+ {8429, L"ERROR_DS_HIERARCHY_TABLE_MALLOC_FAILED"},
+ {8430, L"ERROR_DS_INTERNAL_FAILURE"},
+ {8431, L"ERROR_DS_UNKNOWN_ERROR"},
+ {8432, L"ERROR_DS_ROOT_REQUIRES_CLASS_TOP"},
+ {8433, L"ERROR_DS_REFUSING_FSMO_ROLES"},
+ {8434, L"ERROR_DS_MISSING_FSMO_SETTINGS"},
+ {8435, L"ERROR_DS_UNABLE_TO_SURRENDER_ROLES"},
+ {8436, L"ERROR_DS_DRA_GENERIC"},
+ {8437, L"ERROR_DS_DRA_INVALID_PARAMETER"},
+ {8438, L"ERROR_DS_DRA_BUSY"},
+ {8439, L"ERROR_DS_DRA_BAD_DN"},
+ {8440, L"ERROR_DS_DRA_BAD_NC"},
+ {8441, L"ERROR_DS_DRA_DN_EXISTS"},
+ {8442, L"ERROR_DS_DRA_INTERNAL_ERROR"},
+ {8443, L"ERROR_DS_DRA_INCONSISTENT_DIT"},
+ {8444, L"ERROR_DS_DRA_CONNECTION_FAILED"},
+ {8445, L"ERROR_DS_DRA_BAD_INSTANCE_TYPE"},
+ {8446, L"ERROR_DS_DRA_OUT_OF_MEM"},
+ {8447, L"ERROR_DS_DRA_MAIL_PROBLEM"},
+ {8448, L"ERROR_DS_DRA_REF_ALREADY_EXISTS"},
+ {8449, L"ERROR_DS_DRA_REF_NOT_FOUND"},
+ {8450, L"ERROR_DS_DRA_OBJ_IS_REP_SOURCE"},
+ {8451, L"ERROR_DS_DRA_DB_ERROR"},
+ {8452, L"ERROR_DS_DRA_NO_REPLICA"},
+ {8453, L"ERROR_DS_DRA_ACCESS_DENIED"},
+ {8454, L"ERROR_DS_DRA_NOT_SUPPORTED"},
+ {8455, L"ERROR_DS_DRA_RPC_CANCELLED"},
+ {8456, L"ERROR_DS_DRA_SOURCE_DISABLED"},
+ {8457, L"ERROR_DS_DRA_SINK_DISABLED"},
+ {8458, L"ERROR_DS_DRA_NAME_COLLISION"},
+ {8459, L"ERROR_DS_DRA_SOURCE_REINSTALLED"},
+ {8460, L"ERROR_DS_DRA_MISSING_PARENT"},
+ {8461, L"ERROR_DS_DRA_PREEMPTED"},
+ {8462, L"ERROR_DS_DRA_ABANDON_SYNC"},
+ {8463, L"ERROR_DS_DRA_SHUTDOWN"},
+ {8464, L"ERROR_DS_DRA_INCOMPATIBLE_PARTIAL_SET"},
+ {8465, L"ERROR_DS_DRA_SOURCE_IS_PARTIAL_REPLICA"},
+ {8466, L"ERROR_DS_DRA_EXTN_CONNECTION_FAILED"},
+ {8467, L"ERROR_DS_INSTALL_SCHEMA_MISMATCH"},
+ {8468, L"ERROR_DS_DUP_LINK_ID"},
+ {8469, L"ERROR_DS_NAME_ERROR_RESOLVING"},
+ {8470, L"ERROR_DS_NAME_ERROR_NOT_FOUND"},
+ {8471, L"ERROR_DS_NAME_ERROR_NOT_UNIQUE"},
+ {8472, L"ERROR_DS_NAME_ERROR_NO_MAPPING"},
+ {8473, L"ERROR_DS_NAME_ERROR_DOMAIN_ONLY"},
+ {8474, L"ERROR_DS_NAME_ERROR_NO_SYNTACTICAL_MAPPING"},
+ {8475, L"ERROR_DS_CONSTRUCTED_ATT_MOD"},
+ {8476, L"ERROR_DS_WRONG_OM_OBJ_CLASS"},
+ {8477, L"ERROR_DS_DRA_REPL_PENDING"},
+ {8478, L"ERROR_DS_DS_REQUIRED"},
+ {8479, L"ERROR_DS_INVALID_LDAP_DISPLAY_NAME"},
+ {8480, L"ERROR_DS_NON_BASE_SEARCH"},
+ {8481, L"ERROR_DS_CANT_RETRIEVE_ATTS"},
+ {8482, L"ERROR_DS_BACKLINK_WITHOUT_LINK"},
+ {8483, L"ERROR_DS_EPOCH_MISMATCH"},
+ {8484, L"ERROR_DS_SRC_NAME_MISMATCH"},
+ {8485, L"ERROR_DS_SRC_AND_DST_NC_IDENTICAL"},
+ {8486, L"ERROR_DS_DST_NC_MISMATCH"},
+ {8487, L"ERROR_DS_NOT_AUTHORITIVE_FOR_DST_NC"},
+ {8488, L"ERROR_DS_SRC_GUID_MISMATCH"},
+ {8489, L"ERROR_DS_CANT_MOVE_DELETED_OBJECT"},
+ {8490, L"ERROR_DS_PDC_OPERATION_IN_PROGRESS"},
+ {8491, L"ERROR_DS_CROSS_DOMAIN_CLEANUP_REQD"},
+ {8492, L"ERROR_DS_ILLEGAL_XDOM_MOVE_OPERATION"},
+ {8493, L"ERROR_DS_CANT_WITH_ACCT_GROUP_MEMBERSHPS"},
+ {8494, L"ERROR_DS_NC_MUST_HAVE_NC_PARENT"},
+ {8495, L"ERROR_DS_CR_IMPOSSIBLE_TO_VALIDATE"},
+ {8496, L"ERROR_DS_DST_DOMAIN_NOT_NATIVE"},
+ {8497, L"ERROR_DS_MISSING_INFRASTRUCTURE_CONTAINER"},
+ {8498, L"ERROR_DS_CANT_MOVE_ACCOUNT_GROUP"},
+ {8499, L"ERROR_DS_CANT_MOVE_RESOURCE_GROUP"},
+ {8500, L"ERROR_DS_INVALID_SEARCH_FLAG"},
+ {8501, L"ERROR_DS_NO_TREE_DELETE_ABOVE_NC"},
+ {8502, L"ERROR_DS_COULDNT_LOCK_TREE_FOR_DELETE"},
+ {8503, L"ERROR_DS_COULDNT_IDENTIFY_OBJECTS_FOR_TREE_DELETE"},
+ {8504, L"ERROR_DS_SAM_INIT_FAILURE"},
+ {8505, L"ERROR_DS_SENSITIVE_GROUP_VIOLATION"},
+ {8506, L"ERROR_DS_CANT_MOD_PRIMARYGROUPID"},
+ {8507, L"ERROR_DS_ILLEGAL_BASE_SCHEMA_MOD"},
+ {8508, L"ERROR_DS_NONSAFE_SCHEMA_CHANGE"},
+ {8509, L"ERROR_DS_SCHEMA_UPDATE_DISALLOWED"},
+ {8510, L"ERROR_DS_CANT_CREATE_UNDER_SCHEMA"},
+ {8511, L"ERROR_DS_INSTALL_NO_SRC_SCH_VERSION"},
+ {8512, L"ERROR_DS_INSTALL_NO_SCH_VERSION_IN_INIFILE"},
+ {8513, L"ERROR_DS_INVALID_GROUP_TYPE"},
+ {8514, L"ERROR_DS_NO_NEST_GLOBALGROUP_IN_MIXEDDOMAIN"},
+ {8515, L"ERROR_DS_NO_NEST_LOCALGROUP_IN_MIXEDDOMAIN"},
+ {8516, L"ERROR_DS_GLOBAL_CANT_HAVE_LOCAL_MEMBER"},
+ {8517, L"ERROR_DS_GLOBAL_CANT_HAVE_UNIVERSAL_MEMBER"},
+ {8518, L"ERROR_DS_UNIVERSAL_CANT_HAVE_LOCAL_MEMBER"},
+ {8519, L"ERROR_DS_GLOBAL_CANT_HAVE_CROSSDOMAIN_MEMBER"},
+ {8520, L"ERROR_DS_LOCAL_CANT_HAVE_CROSSDOMAIN_LOCAL_MEMBER"},
+ {8521, L"ERROR_DS_HAVE_PRIMARY_MEMBERS"},
+ {8522, L"ERROR_DS_STRING_SD_CONVERSION_FAILED"},
+ {8523, L"ERROR_DS_NAMING_MASTER_GC"},
+ {8524, L"ERROR_DS_DNS_LOOKUP_FAILURE"},
+ {8525, L"ERROR_DS_COULDNT_UPDATE_SPNS"},
+ {8526, L"ERROR_DS_CANT_RETRIEVE_SD"},
+ {8527, L"ERROR_DS_KEY_NOT_UNIQUE"},
+ {8528, L"ERROR_DS_WRONG_LINKED_ATT_SYNTAX"},
+ {8529, L"ERROR_DS_SAM_NEED_BOOTKEY_PASSWORD"},
+ {8530, L"ERROR_DS_SAM_NEED_BOOTKEY_FLOPPY"},
+ {8531, L"ERROR_DS_CANT_START"},
+ {8532, L"ERROR_DS_INIT_FAILURE"},
+ {8533, L"ERROR_DS_NO_PKT_PRIVACY_ON_CONNECTION"},
+ {8534, L"ERROR_DS_SOURCE_DOMAIN_IN_FOREST"},
+ {8535, L"ERROR_DS_DESTINATION_DOMAIN_NOT_IN_FOREST"},
+ {8536, L"ERROR_DS_DESTINATION_AUDITING_NOT_ENABLED"},
+ {8537, L"ERROR_DS_CANT_FIND_DC_FOR_SRC_DOMAIN"},
+ {8538, L"ERROR_DS_SRC_OBJ_NOT_GROUP_OR_USER"},
+ {8539, L"ERROR_DS_SRC_SID_EXISTS_IN_FOREST"},
+ {8540, L"ERROR_DS_SRC_AND_DST_OBJECT_CLASS_MISMATCH"},
+ {8541, L"ERROR_SAM_INIT_FAILURE"},
+ {8542, L"ERROR_DS_DRA_SCHEMA_INFO_SHIP"},
+ {8543, L"ERROR_DS_DRA_SCHEMA_CONFLICT"},
+ {8544, L"ERROR_DS_DRA_EARLIER_SCHEMA_CONFLICT"},
+ {8545, L"ERROR_DS_DRA_OBJ_NC_MISMATCH"},
+ {8546, L"ERROR_DS_NC_STILL_HAS_DSAS"},
+ {8547, L"ERROR_DS_GC_REQUIRED"},
+ {8548, L"ERROR_DS_LOCAL_MEMBER_OF_LOCAL_ONLY"},
+ {8549, L"ERROR_DS_NO_FPO_IN_UNIVERSAL_GROUPS"},
+ {8550, L"ERROR_DS_CANT_ADD_TO_GC"},
+ {8551, L"ERROR_DS_NO_CHECKPOINT_WITH_PDC"},
+ {8552, L"ERROR_DS_SOURCE_AUDITING_NOT_ENABLED"},
+ {8553, L"ERROR_DS_CANT_CREATE_IN_NONDOMAIN_NC"},
+ {8554, L"ERROR_DS_INVALID_NAME_FOR_SPN"},
+ {8555, L"ERROR_DS_FILTER_USES_CONTRUCTED_ATTRS"},
+ {8556, L"ERROR_DS_UNICODEPWD_NOT_IN_QUOTES"},
+ {8557, L"ERROR_DS_MACHINE_ACCOUNT_QUOTA_EXCEEDED"},
+ {8558, L"ERROR_DS_MUST_BE_RUN_ON_DST_DC"},
+ {8559, L"ERROR_DS_SRC_DC_MUST_BE_SP4_OR_GREATER"},
+ {8560, L"ERROR_DS_CANT_TREE_DELETE_CRITICAL_OBJ"},
+ {8561, L"ERROR_DS_INIT_FAILURE_CONSOLE"},
+ {8562, L"ERROR_DS_SAM_INIT_FAILURE_CONSOLE"},
+ {8563, L"ERROR_DS_FOREST_VERSION_TOO_HIGH"},
+ {8564, L"ERROR_DS_DOMAIN_VERSION_TOO_HIGH"},
+ {8565, L"ERROR_DS_FOREST_VERSION_TOO_LOW"},
+ {8566, L"ERROR_DS_DOMAIN_VERSION_TOO_LOW"},
+ {8567, L"ERROR_DS_INCOMPATIBLE_VERSION"},
+ {8568, L"ERROR_DS_LOW_DSA_VERSION"},
+ {8569, L"ERROR_DS_NO_BEHAVIOR_VERSION_IN_MIXEDDOMAIN"},
+ {8570, L"ERROR_DS_NOT_SUPPORTED_SORT_ORDER"},
+ {8571, L"ERROR_DS_NAME_NOT_UNIQUE"},
+ {8572, L"ERROR_DS_MACHINE_ACCOUNT_CREATED_PRENT4"},
+ {8573, L"ERROR_DS_OUT_OF_VERSION_STORE"},
+ {8574, L"ERROR_DS_INCOMPATIBLE_CONTROLS_USED"},
+ {8575, L"ERROR_DS_NO_REF_DOMAIN"},
+ {8576, L"ERROR_DS_RESERVED_LINK_ID"},
+ {8577, L"ERROR_DS_LINK_ID_NOT_AVAILABLE"},
+ {8578, L"ERROR_DS_AG_CANT_HAVE_UNIVERSAL_MEMBER"},
+ {8579, L"ERROR_DS_MODIFYDN_DISALLOWED_BY_INSTANCE_TYPE"},
+ {8580, L"ERROR_DS_NO_OBJECT_MOVE_IN_SCHEMA_NC"},
+ {8581, L"ERROR_DS_MODIFYDN_DISALLOWED_BY_FLAG"},
+ {8582, L"ERROR_DS_MODIFYDN_WRONG_GRANDPARENT"},
+ {8583, L"ERROR_DS_NAME_ERROR_TRUST_REFERRAL"},
+ {8584, L"ERROR_NOT_SUPPORTED_ON_STANDARD_SERVER"},
+ {8585, L"ERROR_DS_CANT_ACCESS_REMOTE_PART_OF_AD"},
+ {8586, L"ERROR_DS_CR_IMPOSSIBLE_TO_VALIDATE_V2"},
+ {8587, L"ERROR_DS_THREAD_LIMIT_EXCEEDED"},
+ {8588, L"ERROR_DS_NOT_CLOSEST"},
+ {8589, L"ERROR_DS_CANT_DERIVE_SPN_WITHOUT_SERVER_REF"},
+ {8590, L"ERROR_DS_SINGLE_USER_MODE_FAILED"},
+ {8591, L"ERROR_DS_NTDSCRIPT_SYNTAX_ERROR"},
+ {8592, L"ERROR_DS_NTDSCRIPT_PROCESS_ERROR"},
+ {8593, L"ERROR_DS_DIFFERENT_REPL_EPOCHS"},
+ {8594, L"ERROR_DS_DRS_EXTENSIONS_CHANGED"},
+ {8595, L"ERROR_DS_REPLICA_SET_CHANGE_NOT_ALLOWED_ON_DISABLED_CR"},
+ {8596, L"ERROR_DS_NO_MSDS_INTID"},
+ {8597, L"ERROR_DS_DUP_MSDS_INTID"},
+ {8598, L"ERROR_DS_EXISTS_IN_RDNATTID"},
+ {8599, L"ERROR_DS_AUTHORIZATION_FAILED"},
+ {8600, L"ERROR_DS_INVALID_SCRIPT"},
+ {8601, L"ERROR_DS_REMOTE_CROSSREF_OP_FAILED"},
+ {8602, L"ERROR_DS_CROSS_REF_BUSY"},
+ {8603, L"ERROR_DS_CANT_DERIVE_SPN_FOR_DELETED_DOMAIN"},
+ {8604, L"ERROR_DS_CANT_DEMOTE_WITH_WRITEABLE_NC"},
+ {8605, L"ERROR_DS_DUPLICATE_ID_FOUND"},
+ {8606, L"ERROR_DS_INSUFFICIENT_ATTR_TO_CREATE_OBJECT"},
+ {8607, L"ERROR_DS_GROUP_CONVERSION_ERROR"},
+ {8608, L"ERROR_DS_CANT_MOVE_APP_BASIC_GROUP"},
+ {8609, L"ERROR_DS_CANT_MOVE_APP_QUERY_GROUP"},
+ {8610, L"ERROR_DS_ROLE_NOT_VERIFIED"},
+ {8611, L"ERROR_DS_WKO_CONTAINER_CANNOT_BE_SPECIAL"},
+ {8612, L"ERROR_DS_DOMAIN_RENAME_IN_PROGRESS"},
+ {8613, L"ERROR_DS_EXISTING_AD_CHILD_NC"},
+ {8614, L"ERROR_DS_REPL_LIFETIME_EXCEEDED"},
+ {8615, L"ERROR_DS_DISALLOWED_IN_SYSTEM_CONTAINER"},
+ {8616, L"ERROR_DS_LDAP_SEND_QUEUE_FULL"},
+ {8617, L"ERROR_DS_DRA_OUT_SCHEDULE_WINDOW"},
+ {8618, L"ERROR_DS_POLICY_NOT_KNOWN"},
+ {8619, L"ERROR_NO_SITE_SETTINGS_OBJECT"},
+ {8620, L"ERROR_NO_SECRETS"},
+ {8621, L"ERROR_NO_WRITABLE_DC_FOUND"},
+ {8622, L"ERROR_DS_NO_SERVER_OBJECT"},
+ {8623, L"ERROR_DS_NO_NTDSA_OBJECT"},
+ {8624, L"ERROR_DS_NON_ASQ_SEARCH"},
+ {8625, L"ERROR_DS_AUDIT_FAILURE"},
+ {8626, L"ERROR_DS_INVALID_SEARCH_FLAG_SUBTREE"},
+ {8627, L"ERROR_DS_INVALID_SEARCH_FLAG_TUPLE"},
+ {8628, L"ERROR_DS_HIERARCHY_TABLE_TOO_DEEP"},
+ {8629, L"ERROR_DS_DRA_CORRUPT_UTD_VECTOR"},
+ {8630, L"ERROR_DS_DRA_SECRETS_DENIED"},
+ {8631, L"ERROR_DS_RESERVED_MAPI_ID"},
+ {8632, L"ERROR_DS_MAPI_ID_NOT_AVAILABLE"},
+ {8633, L"ERROR_DS_DRA_MISSING_KRBTGT_SECRET"},
+ {8634, L"ERROR_DS_DOMAIN_NAME_EXISTS_IN_FOREST"},
+ {8635, L"ERROR_DS_FLAT_NAME_EXISTS_IN_FOREST"},
+ {8636, L"ERROR_INVALID_USER_PRINCIPAL_NAME"},
+ {8637, L"ERROR_DS_OID_MAPPED_GROUP_CANT_HAVE_MEMBERS"},
+ {8638, L"ERROR_DS_OID_NOT_FOUND"},
+ {8639, L"ERROR_DS_DRA_RECYCLED_TARGET"},
+ {8640, L"ERROR_DS_DISALLOWED_NC_REDIRECT"},
+ {8641, L"ERROR_DS_HIGH_ADLDS_FFL"},
+ {8642, L"ERROR_DS_HIGH_DSA_VERSION"},
+ {8643, L"ERROR_DS_LOW_ADLDS_FFL"},
+ {8644, L"ERROR_DOMAIN_SID_SAME_AS_LOCAL_WORKSTATION"},
+ {8645, L"ERROR_DS_UNDELETE_SAM_VALIDATION_FAILED"},
+ {8646, L"ERROR_INCORRECT_ACCOUNT_TYPE"},
+ {9001, L"DNS_ERROR_RCODE_FORMAT_ERROR"},
+ {9002, L"DNS_ERROR_RCODE_SERVER_FAILURE"},
+ {9003, L"DNS_ERROR_RCODE_NAME_ERROR"},
+ {9004, L"DNS_ERROR_RCODE_NOT_IMPLEMENTED"},
+ {9005, L"DNS_ERROR_RCODE_REFUSED"},
+ {9006, L"DNS_ERROR_RCODE_YXDOMAIN"},
+ {9007, L"DNS_ERROR_RCODE_YXRRSET"},
+ {9008, L"DNS_ERROR_RCODE_NXRRSET"},
+ {9009, L"DNS_ERROR_RCODE_NOTAUTH"},
+ {9010, L"DNS_ERROR_RCODE_NOTZONE"},
+ {9016, L"DNS_ERROR_RCODE_BADSIG"},
+ {9017, L"DNS_ERROR_RCODE_BADKEY"},
+ {9018, L"DNS_ERROR_RCODE_BADTIME"},
+ {9101, L"DNS_ERROR_KEYMASTER_REQUIRED"},
+ {9102, L"DNS_ERROR_NOT_ALLOWED_ON_SIGNED_ZONE"},
+ {9103, L"DNS_ERROR_NSEC3_INCOMPATIBLE_WITH_RSA_SHA1"},
+ {9104, L"DNS_ERROR_NOT_ENOUGH_SIGNING_KEY_DESCRIPTORS"},
+ {9105, L"DNS_ERROR_UNSUPPORTED_ALGORITHM"},
+ {9106, L"DNS_ERROR_INVALID_KEY_SIZE"},
+ {9107, L"DNS_ERROR_SIGNING_KEY_NOT_ACCESSIBLE"},
+ {9108, L"DNS_ERROR_KSP_DOES_NOT_SUPPORT_PROTECTION"},
+ {9109, L"DNS_ERROR_UNEXPECTED_DATA_PROTECTION_ERROR"},
+ {9110, L"DNS_ERROR_UNEXPECTED_CNG_ERROR"},
+ {9111, L"DNS_ERROR_UNKNOWN_SIGNING_PARAMETER_VERSION"},
+ {9112, L"DNS_ERROR_KSP_NOT_ACCESSIBLE"},
+ {9113, L"DNS_ERROR_TOO_MANY_SKDS"},
+ {9114, L"DNS_ERROR_INVALID_ROLLOVER_PERIOD"},
+ {9115, L"DNS_ERROR_INVALID_INITIAL_ROLLOVER_OFFSET"},
+ {9116, L"DNS_ERROR_ROLLOVER_IN_PROGRESS"},
+ {9117, L"DNS_ERROR_STANDBY_KEY_NOT_PRESENT"},
+ {9118, L"DNS_ERROR_NOT_ALLOWED_ON_ZSK"},
+ {9119, L"DNS_ERROR_NOT_ALLOWED_ON_ACTIVE_SKD"},
+ {9120, L"DNS_ERROR_ROLLOVER_ALREADY_QUEUED"},
+ {9121, L"DNS_ERROR_NOT_ALLOWED_ON_UNSIGNED_ZONE"},
+ {9122, L"DNS_ERROR_BAD_KEYMASTER"},
+ {9123, L"DNS_ERROR_INVALID_SIGNATURE_VALIDITY_PERIOD"},
+ {9124, L"DNS_ERROR_INVALID_NSEC3_ITERATION_COUNT"},
+ {9125, L"DNS_ERROR_DNSSEC_IS_DISABLED"},
+ {9126, L"DNS_ERROR_INVALID_XML"},
+ {9127, L"DNS_ERROR_NO_VALID_TRUST_ANCHORS"},
+ {9128, L"DNS_ERROR_ROLLOVER_NOT_POKEABLE"},
+ {9129, L"DNS_ERROR_NSEC3_NAME_COLLISION"},
+ {9130, L"DNS_ERROR_NSEC_INCOMPATIBLE_WITH_NSEC3_RSA_SHA1"},
+ {9501, L"DNS_INFO_NO_RECORDS"},
+ {9502, L"DNS_ERROR_BAD_PACKET"},
+ {9503, L"DNS_ERROR_NO_PACKET"},
+ {9504, L"DNS_ERROR_RCODE"},
+ {9505, L"DNS_ERROR_UNSECURE_PACKET"},
+ {9506, L"DNS_REQUEST_PENDING"},
+ {9551, L"DNS_ERROR_INVALID_TYPE"},
+ {9552, L"DNS_ERROR_INVALID_IP_ADDRESS"},
+ {9553, L"DNS_ERROR_INVALID_PROPERTY"},
+ {9554, L"DNS_ERROR_TRY_AGAIN_LATER"},
+ {9555, L"DNS_ERROR_NOT_UNIQUE"},
+ {9556, L"DNS_ERROR_NON_RFC_NAME"},
+ {9557, L"DNS_STATUS_FQDN"},
+ {9558, L"DNS_STATUS_DOTTED_NAME"},
+ {9559, L"DNS_STATUS_SINGLE_PART_NAME"},
+ {9560, L"DNS_ERROR_INVALID_NAME_CHAR"},
+ {9561, L"DNS_ERROR_NUMERIC_NAME"},
+ {9562, L"DNS_ERROR_NOT_ALLOWED_ON_ROOT_SERVER"},
+ {9563, L"DNS_ERROR_NOT_ALLOWED_UNDER_DELEGATION"},
+ {9564, L"DNS_ERROR_CANNOT_FIND_ROOT_HINTS"},
+ {9565, L"DNS_ERROR_INCONSISTENT_ROOT_HINTS"},
+ {9566, L"DNS_ERROR_DWORD_VALUE_TOO_SMALL"},
+ {9567, L"DNS_ERROR_DWORD_VALUE_TOO_LARGE"},
+ {9568, L"DNS_ERROR_BACKGROUND_LOADING"},
+ {9569, L"DNS_ERROR_NOT_ALLOWED_ON_RODC"},
+ {9570, L"DNS_ERROR_NOT_ALLOWED_UNDER_DNAME"},
+ {9571, L"DNS_ERROR_DELEGATION_REQUIRED"},
+ {9572, L"DNS_ERROR_INVALID_POLICY_TABLE"},
+ {9601, L"DNS_ERROR_ZONE_DOES_NOT_EXIST"},
+ {9602, L"DNS_ERROR_NO_ZONE_INFO"},
+ {9603, L"DNS_ERROR_INVALID_ZONE_OPERATION"},
+ {9604, L"DNS_ERROR_ZONE_CONFIGURATION_ERROR"},
+ {9605, L"DNS_ERROR_ZONE_HAS_NO_SOA_RECORD"},
+ {9606, L"DNS_ERROR_ZONE_HAS_NO_NS_RECORDS"},
+ {9607, L"DNS_ERROR_ZONE_LOCKED"},
+ {9608, L"DNS_ERROR_ZONE_CREATION_FAILED"},
+ {9609, L"DNS_ERROR_ZONE_ALREADY_EXISTS"},
+ {9610, L"DNS_ERROR_AUTOZONE_ALREADY_EXISTS"},
+ {9611, L"DNS_ERROR_INVALID_ZONE_TYPE"},
+ {9612, L"DNS_ERROR_SECONDARY_REQUIRES_MASTER_IP"},
+ {9613, L"DNS_ERROR_ZONE_NOT_SECONDARY"},
+ {9614, L"DNS_ERROR_NEED_SECONDARY_ADDRESSES"},
+ {9615, L"DNS_ERROR_WINS_INIT_FAILED"},
+ {9616, L"DNS_ERROR_NEED_WINS_SERVERS"},
+ {9617, L"DNS_ERROR_NBSTAT_INIT_FAILED"},
+ {9618, L"DNS_ERROR_SOA_DELETE_INVALID"},
+ {9619, L"DNS_ERROR_FORWARDER_ALREADY_EXISTS"},
+ {9620, L"DNS_ERROR_ZONE_REQUIRES_MASTER_IP"},
+ {9621, L"DNS_ERROR_ZONE_IS_SHUTDOWN"},
+ {9622, L"DNS_ERROR_ZONE_LOCKED_FOR_SIGNING"},
+ {9651, L"DNS_ERROR_PRIMARY_REQUIRES_DATAFILE"},
+ {9652, L"DNS_ERROR_INVALID_DATAFILE_NAME"},
+ {9653, L"DNS_ERROR_DATAFILE_OPEN_FAILURE"},
+ {9654, L"DNS_ERROR_FILE_WRITEBACK_FAILED"},
+ {9655, L"DNS_ERROR_DATAFILE_PARSING"},
+ {9701, L"DNS_ERROR_RECORD_DOES_NOT_EXIST"},
+ {9702, L"DNS_ERROR_RECORD_FORMAT"},
+ {9703, L"DNS_ERROR_NODE_CREATION_FAILED"},
+ {9704, L"DNS_ERROR_UNKNOWN_RECORD_TYPE"},
+ {9705, L"DNS_ERROR_RECORD_TIMED_OUT"},
+ {9706, L"DNS_ERROR_NAME_NOT_IN_ZONE"},
+ {9707, L"DNS_ERROR_CNAME_LOOP"},
+ {9708, L"DNS_ERROR_NODE_IS_CNAME"},
+ {9709, L"DNS_ERROR_CNAME_COLLISION"},
+ {9710, L"DNS_ERROR_RECORD_ONLY_AT_ZONE_ROOT"},
+ {9711, L"DNS_ERROR_RECORD_ALREADY_EXISTS"},
+ {9712, L"DNS_ERROR_SECONDARY_DATA"},
+ {9713, L"DNS_ERROR_NO_CREATE_CACHE_DATA"},
+ {9714, L"DNS_ERROR_NAME_DOES_NOT_EXIST"},
+ {9715, L"DNS_WARNING_PTR_CREATE_FAILED"},
+ {9716, L"DNS_WARNING_DOMAIN_UNDELETED"},
+ {9717, L"DNS_ERROR_DS_UNAVAILABLE"},
+ {9718, L"DNS_ERROR_DS_ZONE_ALREADY_EXISTS"},
+ {9719, L"DNS_ERROR_NO_BOOTFILE_IF_DS_ZONE"},
+ {9720, L"DNS_ERROR_NODE_IS_DNAME"},
+ {9721, L"DNS_ERROR_DNAME_COLLISION"},
+ {9722, L"DNS_ERROR_ALIAS_LOOP"},
+ {9751, L"DNS_INFO_AXFR_COMPLETE"},
+ {9752, L"DNS_ERROR_AXFR"},
+ {9753, L"DNS_INFO_ADDED_LOCAL_WINS"},
+ {9801, L"DNS_STATUS_CONTINUE_NEEDED"},
+ {9851, L"DNS_ERROR_NO_TCPIP"},
+ {9852, L"DNS_ERROR_NO_DNS_SERVERS"},
+ {9901, L"DNS_ERROR_DP_DOES_NOT_EXIST"},
+ {9902, L"DNS_ERROR_DP_ALREADY_EXISTS"},
+ {9903, L"DNS_ERROR_DP_NOT_ENLISTED"},
+ {9904, L"DNS_ERROR_DP_ALREADY_ENLISTED"},
+ {9905, L"DNS_ERROR_DP_NOT_AVAILABLE"},
+ {9906, L"DNS_ERROR_DP_FSMO_ERROR"},
+ {10004, L"WSAEINTR"},
+ {10009, L"WSAEBADF"},
+ {10013, L"WSAEACCES"},
+ {10014, L"WSAEFAULT"},
+ {10022, L"WSAEINVAL"},
+ {10024, L"WSAEMFILE"},
+ {10035, L"WSAEWOULDBLOCK"},
+ {10036, L"WSAEINPROGRESS"},
+ {10037, L"WSAEALREADY"},
+ {10038, L"WSAENOTSOCK"},
+ {10039, L"WSAEDESTADDRREQ"},
+ {10040, L"WSAEMSGSIZE"},
+ {10041, L"WSAEPROTOTYPE"},
+ {10042, L"WSAENOPROTOOPT"},
+ {10043, L"WSAEPROTONOSUPPORT"},
+ {10044, L"WSAESOCKTNOSUPPORT"},
+ {10045, L"WSAEOPNOTSUPP"},
+ {10046, L"WSAEPFNOSUPPORT"},
+ {10047, L"WSAEAFNOSUPPORT"},
+ {10048, L"WSAEADDRINUSE"},
+ {10049, L"WSAEADDRNOTAVAIL"},
+ {10050, L"WSAENETDOWN"},
+ {10051, L"WSAENETUNREACH"},
+ {10052, L"WSAENETRESET"},
+ {10053, L"WSAECONNABORTED"},
+ {10054, L"WSAECONNRESET"},
+ {10055, L"WSAENOBUFS"},
+ {10056, L"WSAEISCONN"},
+ {10057, L"WSAENOTCONN"},
+ {10058, L"WSAESHUTDOWN"},
+ {10059, L"WSAETOOMANYREFS"},
+ {10060, L"WSAETIMEDOUT"},
+ {10061, L"WSAECONNREFUSED"},
+ {10062, L"WSAELOOP"},
+ {10063, L"WSAENAMETOOLONG"},
+ {10064, L"WSAEHOSTDOWN"},
+ {10065, L"WSAEHOSTUNREACH"},
+ {10066, L"WSAENOTEMPTY"},
+ {10067, L"WSAEPROCLIM"},
+ {10068, L"WSAEUSERS"},
+ {10069, L"WSAEDQUOT"},
+ {10070, L"WSAESTALE"},
+ {10071, L"WSAEREMOTE"},
+ {10091, L"WSASYSNOTREADY"},
+ {10092, L"WSAVERNOTSUPPORTED"},
+ {10093, L"WSANOTINITIALISED"},
+ {10101, L"WSAEDISCON"},
+ {10102, L"WSAENOMORE"},
+ {10103, L"WSAECANCELLED"},
+ {10104, L"WSAEINVALIDPROCTABLE"},
+ {10105, L"WSAEINVALIDPROVIDER"},
+ {10106, L"WSAEPROVIDERFAILEDINIT"},
+ {10107, L"WSASYSCALLFAILURE"},
+ {10108, L"WSASERVICE_NOT_FOUND"},
+ {10109, L"WSATYPE_NOT_FOUND"},
+ {10110, L"WSA_E_NO_MORE"},
+ {10111, L"WSA_E_CANCELLED"},
+ {10112, L"WSAEREFUSED"},
+ {11001, L"WSAHOST_NOT_FOUND"},
+ {11002, L"WSATRY_AGAIN"},
+ {11003, L"WSANO_RECOVERY"},
+ {11004, L"WSANO_DATA"},
+ {11005, L"WSA_QOS_RECEIVERS"},
+ {11006, L"WSA_QOS_SENDERS"},
+ {11007, L"WSA_QOS_NO_SENDERS"},
+ {11008, L"WSA_QOS_NO_RECEIVERS"},
+ {11009, L"WSA_QOS_REQUEST_CONFIRMED"},
+ {11010, L"WSA_QOS_ADMISSION_FAILURE"},
+ {11011, L"WSA_QOS_POLICY_FAILURE"},
+ {11012, L"WSA_QOS_BAD_STYLE"},
+ {11013, L"WSA_QOS_BAD_OBJECT"},
+ {11014, L"WSA_QOS_TRAFFIC_CTRL_ERROR"},
+ {11015, L"WSA_QOS_GENERIC_ERROR"},
+ {11016, L"WSA_QOS_ESERVICETYPE"},
+ {11017, L"WSA_QOS_EFLOWSPEC"},
+ {11018, L"WSA_QOS_EPROVSPECBUF"},
+ {11019, L"WSA_QOS_EFILTERSTYLE"},
+ {11020, L"WSA_QOS_EFILTERTYPE"},
+ {11021, L"WSA_QOS_EFILTERCOUNT"},
+ {11022, L"WSA_QOS_EOBJLENGTH"},
+ {11023, L"WSA_QOS_EFLOWCOUNT"},
+ {11024, L"WSA_QOS_EUNKOWNPSOBJ"},
+ {11025, L"WSA_QOS_EPOLICYOBJ"},
+ {11026, L"WSA_QOS_EFLOWDESC"},
+ {11027, L"WSA_QOS_EPSFLOWSPEC"},
+ {11028, L"WSA_QOS_EPSFILTERSPEC"},
+ {11029, L"WSA_QOS_ESDMODEOBJ"},
+ {11030, L"WSA_QOS_ESHAPERATEOBJ"},
+ {11031, L"WSA_QOS_RESERVED_PETYPE"},
+ {11032, L"WSA_SECURE_HOST_NOT_FOUND"},
+ {11033, L"WSA_IPSEC_NAME_POLICY_ERROR"}};
+
+const wchar_t* errorCodeName(DWORD code)
+{
+ static const wchar_t* NotFound = L"unknown error";
+
+ auto itor = std::lower_bound(std::begin(g_codes), std::end(g_codes), CodeName{code},
+ [](auto&& a, auto&& b) {
+ return (a.code < b.code);
+ });
+
+ if (itor != std::end(g_codes)) {
+ return itor->name;
+ }
+
+ return NotFound;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/eventfilter.cpp b/libs/uibase/src/eventfilter.cpp
new file mode 100644
index 0000000..3b624c7
--- /dev/null
+++ b/libs/uibase/src/eventfilter.cpp
@@ -0,0 +1,34 @@
+/*
+Copyright (C) 2016 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/eventfilter.h>
+
+namespace MOBase
+{
+
+EventFilter::EventFilter(QObject* parent, const EventFilter::HandlerFunc& handler)
+ : QObject(parent), m_Handler(handler)
+{}
+
+bool EventFilter::eventFilter(QObject* obj, QEvent* event)
+{
+ return m_Handler(obj, event);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/executableinfo.cpp b/libs/uibase/src/executableinfo.cpp
new file mode 100644
index 0000000..1573a0b
--- /dev/null
+++ b/libs/uibase/src/executableinfo.cpp
@@ -0,0 +1,105 @@
+#include <uibase/executableinfo.h>
+
+using namespace MOBase;
+
+ExecutableForcedLoadSetting::ExecutableForcedLoadSetting(const QString& process,
+ const QString& library)
+ : m_Enabled(false), m_Process(process), m_Library(library), m_Forced(false)
+{}
+
+ExecutableForcedLoadSetting& ExecutableForcedLoadSetting::withEnabled(bool enabled)
+{
+ m_Enabled = enabled;
+ return *this;
+}
+
+ExecutableForcedLoadSetting& ExecutableForcedLoadSetting::withForced(bool forced)
+{
+ m_Forced = forced;
+ return *this;
+}
+
+bool ExecutableForcedLoadSetting::enabled() const
+{
+ return m_Enabled;
+}
+
+bool ExecutableForcedLoadSetting::forced() const
+{
+ return m_Forced;
+}
+
+QString ExecutableForcedLoadSetting::library() const
+{
+ return m_Library;
+}
+
+QString ExecutableForcedLoadSetting::process() const
+{
+ return m_Process;
+}
+
+MOBase::ExecutableInfo::ExecutableInfo(const QString& title, const QFileInfo& binary)
+ : m_Title(title), m_Binary(binary),
+ m_WorkingDirectory(binary.exists() ? binary.absoluteDir() : QString()),
+ m_SteamAppID()
+{}
+
+ExecutableInfo& ExecutableInfo::withArgument(const QString& argument)
+{
+ m_Arguments.append(argument);
+ return *this;
+}
+
+ExecutableInfo& ExecutableInfo::withWorkingDirectory(const QDir& workingDirectory)
+{
+ m_WorkingDirectory = workingDirectory;
+ return *this;
+}
+
+ExecutableInfo& MOBase::ExecutableInfo::withSteamAppId(const QString& appId)
+{
+ m_SteamAppID = appId;
+ return *this;
+}
+
+ExecutableInfo& ExecutableInfo::asCustom()
+{
+ m_Custom = true;
+ return *this;
+}
+
+bool ExecutableInfo::isValid() const
+{
+ return m_Binary.exists();
+}
+
+QString ExecutableInfo::title() const
+{
+ return m_Title;
+}
+
+QFileInfo ExecutableInfo::binary() const
+{
+ return m_Binary;
+}
+
+QStringList ExecutableInfo::arguments() const
+{
+ return m_Arguments;
+}
+
+QDir ExecutableInfo::workingDirectory() const
+{
+ return m_WorkingDirectory;
+}
+
+QString ExecutableInfo::steamAppID() const
+{
+ return m_SteamAppID;
+}
+
+bool ExecutableInfo::isCustom() const
+{
+ return m_Custom;
+}
diff --git a/libs/uibase/src/expanderwidget.cpp b/libs/uibase/src/expanderwidget.cpp
new file mode 100644
index 0000000..451ab07
--- /dev/null
+++ b/libs/uibase/src/expanderwidget.cpp
@@ -0,0 +1,93 @@
+#include <uibase/expanderwidget.h>
+#include <QIODevice>
+
+namespace MOBase
+{
+
+ExpanderWidget::ExpanderWidget() : m_button(nullptr), m_content(nullptr), opened_(false)
+{}
+
+ExpanderWidget::ExpanderWidget(QToolButton* button, QWidget* content) : ExpanderWidget()
+{
+ set(button, content);
+}
+
+void ExpanderWidget::set(QToolButton* button, QWidget* content, bool o)
+{
+ m_button = button;
+ m_content = content;
+
+ m_button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ QObject::connect(m_button, &QToolButton::clicked, [&] {
+ toggle();
+ });
+
+ toggle(o);
+}
+
+void ExpanderWidget::toggle()
+{
+ if (opened()) {
+ toggle(false);
+ } else {
+ toggle(true);
+ }
+}
+
+void ExpanderWidget::toggle(bool b)
+{
+ if (b != opened_) {
+ emit aboutToToggle(b);
+ }
+
+ if (b) {
+ m_button->setArrowType(Qt::DownArrow);
+ m_content->show();
+ } else {
+ m_button->setArrowType(Qt::RightArrow);
+ m_content->hide();
+ }
+
+ if (b != opened_) {
+ // the state has to be remembered instead of using m_content's visibility
+ // because saving the state in saveConflictExpandersState() happens after the
+ // dialog is closed, which marks all the widgets hidden
+ opened_ = b;
+
+ emit toggled(b);
+ }
+}
+
+bool ExpanderWidget::opened() const
+{
+ return opened_;
+}
+
+QByteArray ExpanderWidget::saveState() const
+{
+ QByteArray result;
+ QDataStream stream(&result, QIODevice::WriteOnly);
+
+ stream << opened();
+
+ return result;
+}
+
+void ExpanderWidget::restoreState(const QByteArray& a)
+{
+ QDataStream stream(a);
+
+ bool opened = false;
+ stream >> opened;
+
+ if (stream.status() == QDataStream::Ok) {
+ toggle(opened);
+ }
+}
+
+QToolButton* ExpanderWidget::button() const
+{
+ return m_button;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/filesystemutilities.cpp b/libs/uibase/src/filesystemutilities.cpp
new file mode 100644
index 0000000..6a01324
--- /dev/null
+++ b/libs/uibase/src/filesystemutilities.cpp
@@ -0,0 +1,68 @@
+#include <uibase/filesystemutilities.h>
+
+#include <QRegularExpression>
+#include <QString>
+
+namespace MOBase
+{
+
+bool fixDirectoryName(QString& name)
+{
+ QString temp = name.simplified();
+ while (temp.endsWith('.'))
+ temp.chop(1);
+
+ temp.replace(QRegularExpression(R"([<>:"/\\|?*])"), "");
+ static QString invalidNames[] = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2",
+ "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
+ "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
+ "LPT6", "LPT7", "LPT8", "LPT9"};
+ for (unsigned int i = 0; i < sizeof(invalidNames) / sizeof(QString); ++i) {
+ if (temp == invalidNames[i]) {
+ temp = "";
+ break;
+ }
+ }
+
+ temp = temp.simplified();
+
+ if (temp.length() >= 1) {
+ name = temp;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+QString sanitizeFileName(const QString& name, const QString& replacement)
+{
+ QString new_name = name;
+
+ // Remove characters not allowed by Windows
+ new_name.replace(QRegularExpression("[\\x{00}-\\x{1f}\\\\/:\\*\\?\"<>|]"),
+ replacement);
+
+ // Don't end with a period or a space
+ // Don't be "." or ".."
+ new_name.remove(QRegularExpression("[\\. ]*$"));
+
+ // Recurse until stuff stops changing
+ if (new_name != name) {
+ return sanitizeFileName(new_name);
+ }
+ return new_name;
+}
+
+bool validFileName(const QString& name)
+{
+ if (name.isEmpty()) {
+ return false;
+ }
+ if (name == "." || name == "..") {
+ return false;
+ }
+
+ return (name == sanitizeFileName(name));
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/filterwidget.cpp b/libs/uibase/src/filterwidget.cpp
new file mode 100644
index 0000000..cb0c503
--- /dev/null
+++ b/libs/uibase/src/filterwidget.cpp
@@ -0,0 +1,693 @@
+#include <uibase/filterwidget.h>
+#include <uibase/eventfilter.h>
+#include <uibase/log.h>
+#include <QApplication>
+#include <QContextMenuEvent>
+#include <QDialog>
+#include <QEvent>
+#include <QLineEdit>
+#include <QMenu>
+#include <QSortFilterProxyModel>
+#include <QTimer>
+
+namespace MOBase
+{
+
+void setStyleProperty(QWidget* w, const char* k, const QVariant& v)
+{
+ w->setProperty(k, v);
+
+ if (auto* s = w->style()) {
+ // changing properties doesn't repolish automatically
+ s->unpolish(w);
+ s->polish(w);
+ }
+
+ // the qss will probably change the border, which requires sending a manual
+ // StyleChange because the box model has changed
+ QEvent event(QEvent::StyleChange);
+ QApplication::sendEvent(w, &event);
+ w->update();
+ w->updateGeometry();
+}
+
+FilterWidgetProxyModel::FilterWidgetProxyModel(FilterWidget& fw, QWidget* parent)
+ : QSortFilterProxyModel(parent), m_filter(fw)
+{
+ setRecursiveFilteringEnabled(true);
+}
+
+bool FilterWidgetProxyModel::filterAcceptsRow(int sourceRow,
+ const QModelIndex& sourceParent) const
+{
+ if (!m_filter.filteringEnabled()) {
+ return true;
+ }
+
+ const auto cols = sourceModel()->columnCount();
+
+ const auto m = m_filter.matches([&](auto&& regex) {
+ if (m_filter.filterColumn() == -1) {
+ for (int c = 0; c < cols; ++c) {
+ if (columnMatches(sourceRow, sourceParent, c, regex)) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ return columnMatches(sourceRow, sourceParent, m_filter.filterColumn(), regex);
+ }
+ });
+
+ return m;
+}
+
+bool FilterWidgetProxyModel::columnMatches(int sourceRow,
+ const QModelIndex& sourceParent, int c,
+ const QRegularExpression& regex) const
+{
+ QModelIndex index = sourceModel()->index(sourceRow, c, sourceParent);
+ const auto text = sourceModel()->data(index, Qt::DisplayRole).toString();
+
+ return regex.match(text).hasMatch();
+}
+
+void FilterWidgetProxyModel::sort(int column, Qt::SortOrder order)
+{
+ if (m_filter.useSourceSort()) {
+ // without this, the proxy seems to sometimes ignore the sorting done below,
+ // no idea why
+ QSortFilterProxyModel::sort(-1, order);
+
+ sourceModel()->sort(column, order);
+ } else {
+ QSortFilterProxyModel::sort(column, order);
+ }
+}
+
+bool FilterWidgetProxyModel::lessThan(const QModelIndex& left,
+ const QModelIndex& right) const
+{
+ if (auto lt = m_filter.sortPredicate()) {
+ return lt(left, right);
+ } else {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+}
+
+static FilterWidget::Options s_options;
+
+FilterWidget::FilterWidget()
+ : m_edit(nullptr), m_list(nullptr), m_proxy(nullptr), m_eventFilter(nullptr),
+ m_clear(nullptr), m_timer(nullptr), m_useDelay(false), m_valid(true),
+ m_useSourceSort(false), m_filterColumn(-1), m_filteringEnabled(true),
+ m_filteredBorder(true)
+{
+ m_timer = new QTimer(this);
+ m_timer->setSingleShot(true);
+ QObject::connect(
+ m_timer, &QTimer::timeout, this,
+ [&] {
+ set();
+ },
+ Qt::QueuedConnection);
+}
+
+void FilterWidget::setOptions(const Options& o)
+{
+ s_options = o;
+}
+
+FilterWidget::Options FilterWidget::options()
+{
+ return s_options;
+}
+
+void FilterWidget::setEdit(QLineEdit* edit)
+{
+ unhookEdit();
+
+ m_edit = edit;
+
+ if (m_edit) {
+ m_edit->setPlaceholderText(QObject::tr("Filter"));
+
+ createClear();
+ hookEdit();
+ clear();
+ }
+}
+
+void FilterWidget::setList(QAbstractItemView* list)
+{
+ unhookList();
+
+ m_list = list;
+
+ if (list) {
+ hookList();
+ } else {
+ m_proxy = nullptr;
+ }
+}
+
+void FilterWidget::clear()
+{
+ if (!m_edit) {
+ return;
+ }
+
+ m_edit->clear();
+}
+
+void FilterWidget::scrollToSelection()
+{
+ if (options().scrollToSelection && m_list &&
+ m_list->selectionModel()->hasSelection()) {
+ m_list->scrollTo(m_list->selectionModel()->selectedIndexes()[0]);
+ }
+}
+
+bool FilterWidget::empty() const
+{
+ return m_text.isEmpty();
+}
+
+void FilterWidget::setUpdateDelay(bool b)
+{
+ m_useDelay = b;
+}
+
+bool FilterWidget::hasUpdateDelay() const
+{
+ return m_useDelay;
+}
+
+void FilterWidget::setUseSourceSort(bool b)
+{
+ m_useSourceSort = b;
+}
+
+bool FilterWidget::useSourceSort() const
+{
+ return m_useSourceSort;
+}
+
+void FilterWidget::setSortPredicate(sortFun f)
+{
+ m_lt = f;
+}
+
+const FilterWidget::sortFun& FilterWidget::sortPredicate() const
+{
+ return m_lt;
+}
+
+void FilterWidget::setFilterColumn(int i)
+{
+ m_filterColumn = i;
+}
+
+int FilterWidget::filterColumn() const
+{
+ return m_filterColumn;
+}
+
+void FilterWidget::setFilteringEnabled(bool b)
+{
+ m_filteringEnabled = b;
+}
+
+bool FilterWidget::filteringEnabled() const
+{
+ return m_filteringEnabled;
+}
+
+void FilterWidget::setFilteredBorder(bool b)
+{
+ m_filteredBorder = b;
+}
+
+bool FilterWidget::filteredBorder() const
+{
+ return m_filteredBorder;
+}
+
+FilterWidgetProxyModel* FilterWidget::proxyModel()
+{
+ return m_proxy;
+}
+
+QAbstractItemModel* FilterWidget::sourceModel()
+{
+ if (m_proxy) {
+ return m_proxy->sourceModel();
+ } else if (m_list) {
+ return m_list->model();
+ } else {
+ return nullptr;
+ }
+}
+
+QModelIndex FilterWidget::mapFromSource(const QModelIndex& index) const
+{
+ if (m_proxy) {
+ return m_proxy->mapFromSource(index);
+ } else {
+ log::error("FilterWidget::mapFromSource() called, but proxy isn't set up");
+ return index;
+ }
+}
+
+QModelIndex FilterWidget::mapToSource(const QModelIndex& index) const
+{
+ if (m_proxy) {
+ return m_proxy->mapToSource(index);
+ } else {
+ log::error("FilterWidget::mapToSource() called, but proxy isn't set up");
+ return index;
+ }
+}
+
+QItemSelection FilterWidget::mapSelectionFromSource(const QItemSelection& sel) const
+{
+ if (m_proxy) {
+ return m_proxy->mapSelectionFromSource(sel);
+ } else {
+ log::error("FilterWidget::mapToSource() called, but proxy isn't set up");
+ return sel;
+ }
+}
+
+QItemSelection FilterWidget::mapSelectionToSource(const QItemSelection& sel) const
+{
+ if (m_proxy) {
+ return m_proxy->mapSelectionToSource(sel);
+ } else {
+ log::error("FilterWidget::mapToSource() called, but proxy isn't set up");
+ return sel;
+ }
+}
+
+void FilterWidget::compile()
+{
+ Compiled compiled;
+
+ if (s_options.useRegex) {
+ QRegularExpression::PatternOptions flags =
+ QRegularExpression::DotMatchesEverythingOption;
+
+ if (!s_options.regexCaseSensitive) {
+ flags |= QRegularExpression::CaseInsensitiveOption;
+ }
+
+ if (s_options.regexExtended) {
+ flags |= QRegularExpression::ExtendedPatternSyntaxOption;
+ }
+
+ compiled.push_back({QRegularExpression(m_text, flags)});
+ } else {
+ const QStringList ORList = [&] {
+ QString filterCopy = QString(m_text);
+ filterCopy.replace("||", ";").replace("OR", ";").replace("|", ";");
+ return filterCopy.split(";", Qt::SkipEmptyParts);
+ }();
+
+ // split in ORSegments that internally use AND logic
+ for (const auto& ORSegment : ORList) {
+ const auto keywords = ORSegment.split(" ", Qt::SkipEmptyParts);
+ QList<QRegularExpression> regexes;
+
+ for (const auto& keyword : keywords) {
+ const QString escaped = QRegularExpression::escape(keyword);
+
+ const auto flags = QRegularExpression::CaseInsensitiveOption |
+ QRegularExpression::DotMatchesEverythingOption;
+
+ regexes.push_back(QRegularExpression(escaped, flags));
+ }
+
+ compiled.push_back(regexes);
+ }
+ }
+
+ bool valid = true;
+
+ for (auto&& ANDKeywords : compiled) {
+ for (auto&& keyword : ANDKeywords) {
+ if (!keyword.isValid()) {
+ valid = false;
+ break;
+ }
+ }
+
+ if (!valid) {
+ break;
+ }
+ }
+
+ m_valid = valid;
+ setStyleProperty(m_edit, "valid-filter", m_valid);
+
+ if (m_valid) {
+ m_compiled = std::move(compiled);
+ }
+}
+
+bool FilterWidget::matches(predFun pred) const
+{
+ if (m_compiled.isEmpty() || !pred) {
+ return true;
+ }
+
+ for (auto& ANDKeywords : m_compiled) {
+ bool segmentGood = true;
+
+ // check each word in the segment for match, each word needs to be matched
+ // but it doesn't matter where.
+ for (auto& currentKeyword : ANDKeywords) {
+ if (!pred(currentKeyword)) {
+ segmentGood = false;
+ }
+ }
+
+ if (segmentGood) {
+ // the last AND loop didn't break so the ORSegments is true so mod
+ // matches filter
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool FilterWidget::matches(const QString& s) const
+{
+ return matches([&](const QRegularExpression& re) {
+ return re.match(s).hasMatch();
+ });
+}
+
+void FilterWidget::hookEdit()
+{
+ m_eventFilter = new EventFilter(m_edit, [&](auto* w, auto* e) {
+ if (e->type() == QEvent::Resize) {
+ onResized();
+ } else if (e->type() == QEvent::ContextMenu) {
+ onContextMenu(w, static_cast<QContextMenuEvent*>(e));
+ return true;
+ }
+
+ return false;
+ });
+
+ m_edit->installEventFilter(m_eventFilter);
+ QObject::connect(m_edit, &QLineEdit::textChanged, [&] {
+ onTextChanged();
+ });
+}
+
+void FilterWidget::unhookEdit()
+{
+ if (m_clear) {
+ delete m_clear;
+ m_clear = nullptr;
+ }
+
+ if (m_edit) {
+ m_edit->removeEventFilter(m_eventFilter);
+ }
+}
+
+void FilterWidget::hookList()
+{
+ m_proxy = new FilterWidgetProxyModel(*this);
+ m_proxy->setSourceModel(m_list->model());
+ m_list->setModel(m_proxy);
+
+ setShortcuts();
+}
+
+void FilterWidget::unhookList()
+{
+ if (m_proxy && m_list) {
+ auto* model = m_proxy->sourceModel();
+ m_proxy->setSourceModel(nullptr);
+ delete m_proxy;
+
+ m_list->setModel(model);
+ }
+
+ for (auto* s : m_shortcuts) {
+ delete s;
+ }
+
+ m_shortcuts.clear();
+}
+
+// walks up the parents of `w`, returns true if one of them is a QDialog
+//
+bool topLevelIsDialog(QWidget* w)
+{
+ if (!w) {
+ return false;
+ }
+
+ auto* p = w->parentWidget();
+ while (p) {
+ if (dynamic_cast<QDialog*>(p)) {
+ return true;
+ }
+
+ p = p->parentWidget();
+ }
+
+ return false;
+}
+
+void FilterWidget::setShortcuts()
+{
+ auto activate = [this] {
+ onFind();
+ };
+
+ auto reset = [this] {
+ onReset();
+ };
+
+ auto hookActivate = [&](auto* w) {
+ auto* s = new QShortcut(QKeySequence::Find, w);
+ s->setAutoRepeat(false);
+ s->setContext(Qt::WidgetWithChildrenShortcut);
+ QObject::connect(s, &QShortcut::activated, activate);
+ m_shortcuts.push_back(s);
+ };
+
+ auto hookReset = [&](auto* w) {
+ auto* s = new QShortcut(QKeySequence(Qt::Key_Escape), w);
+ s->setAutoRepeat(false);
+ s->setContext(Qt::WidgetWithChildrenShortcut);
+ QObject::connect(s, &QShortcut::activated, reset);
+ m_shortcuts.push_back(s);
+ };
+
+ // don't hook the escape key for reset when the filter is in a dialog, the
+ // standard behaviour is to close the dialog
+ const bool inDialog =
+ topLevelIsDialog(m_list ? static_cast<QWidget*>(m_list) : m_edit);
+
+ if (m_list) {
+ hookActivate(m_list);
+
+ if (!inDialog) {
+ hookReset(m_list);
+ }
+ }
+
+ if (m_edit) {
+ hookActivate(m_edit);
+
+ if (!inDialog) {
+ hookReset(m_edit);
+ }
+ }
+}
+
+void FilterWidget::onFind()
+{
+ if (m_edit) {
+ m_edit->setFocus();
+ m_edit->selectAll();
+ }
+}
+
+void FilterWidget::onReset()
+{
+ clear();
+
+ if (m_list) {
+ m_list->setFocus();
+ }
+}
+
+void FilterWidget::createClear()
+{
+ m_clear = new QToolButton(m_edit);
+
+ QPixmap pixmap(":/MO/gui/edit_clear");
+ m_clear->setIcon(QIcon(pixmap));
+ m_clear->setIconSize(pixmap.size());
+ m_clear->setCursor(Qt::ArrowCursor);
+ m_clear->setStyleSheet("QToolButton { border: none; padding: 0px; }");
+ m_clear->hide();
+
+ QObject::connect(m_clear, &QToolButton::clicked, [&] {
+ clear();
+ });
+
+ repositionClearButton();
+}
+
+void FilterWidget::set()
+{
+ const QString old = m_text;
+
+ QString currentText;
+ if (m_edit != nullptr) {
+ currentText = m_edit->text();
+ } else {
+ currentText = m_text;
+ }
+
+ emit aboutToChange(old, currentText);
+
+ m_text = currentText;
+ compile();
+
+ if (m_proxy) {
+ m_proxy->refreshFilter();
+ }
+
+ if (m_list) {
+ if (m_filteredBorder) {
+ setStyleProperty(m_list, "filtered", !m_text.isEmpty());
+ }
+
+ scrollToSelection();
+ }
+
+ emit changed(old, currentText);
+}
+
+void FilterWidget::update()
+{
+ if (!m_text.isEmpty()) {
+ set();
+ }
+}
+
+void FilterWidget::onTextChanged()
+{
+ m_clear->setVisible(!m_edit->text().isEmpty());
+
+ const auto text = m_edit->text();
+ if (text == m_text) {
+ return;
+ }
+
+ if (m_useDelay) {
+ m_timer->start(500);
+ } else {
+ set();
+ }
+}
+
+void FilterWidget::onResized()
+{
+ repositionClearButton();
+}
+
+void FilterWidget::onContextMenu(QObject*, QContextMenuEvent* e)
+{
+ std::unique_ptr<QMenu> m(m_edit->createStandardContextMenu());
+ m->setParent(m_edit);
+
+ auto* title = new QAction(tr("Filter options"), m_edit);
+ title->setEnabled(false);
+
+ auto f = title->font();
+ f.setBold(true);
+ title->setFont(f);
+
+ auto* regex = new QAction(tr("Use regular expressions"), m_edit);
+ regex->setStatusTip(tr("Use regular expressions in filters"));
+ regex->setCheckable(true);
+ regex->setChecked(s_options.useRegex);
+
+ connect(regex, &QAction::triggered, [&] {
+ s_options.useRegex = regex->isChecked();
+ update();
+ });
+
+ auto* cs = new QAction(tr("Case sensitive"), m_edit);
+ //: leave "(/i)" verbatim
+ cs->setStatusTip(tr("Make regular expressions case sensitive (/i)"));
+ cs->setCheckable(true);
+ cs->setChecked(s_options.regexCaseSensitive);
+ cs->setEnabled(s_options.useRegex);
+
+ connect(cs, &QAction::triggered, [&] {
+ s_options.regexCaseSensitive = cs->isChecked();
+ update();
+ });
+
+ auto* x = new QAction(tr("Extended"), m_edit);
+ //: leave "(/x)" verbatim
+ x->setStatusTip(tr("Ignores unescaped whitespace in regular expressions (/x)"));
+ x->setCheckable(true);
+ x->setChecked(s_options.regexExtended);
+ x->setEnabled(s_options.useRegex);
+
+ connect(x, &QAction::triggered, [&] {
+ s_options.regexExtended = x->isChecked();
+ update();
+ });
+
+ auto* sts = new QAction(tr("Keep selection in view"), m_edit);
+ sts->setStatusTip(tr("Scroll to keep the current selection in view after filtering"));
+ sts->setCheckable(true);
+ sts->setChecked(s_options.scrollToSelection);
+
+ connect(sts, &QAction::triggered, [&] {
+ s_options.scrollToSelection = sts->isChecked();
+ update();
+ });
+
+ m->insertSeparator(m->actions().first());
+ m->insertAction(m->actions().first(), x);
+ m->insertAction(m->actions().first(), cs);
+ m->insertAction(m->actions().first(), regex);
+ m->insertAction(m->actions().first(), sts);
+ m->insertAction(m->actions().first(), title);
+
+ m->exec(e->globalPos());
+}
+
+void FilterWidget::repositionClearButton()
+{
+ if (!m_clear) {
+ return;
+ }
+
+ const QSize sz = m_clear->sizeHint();
+ const int frame = m_edit->style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
+ const auto r = m_edit->rect();
+
+ const auto x = r.right() - frame - sz.width();
+ const auto y = (r.bottom() + 1 - sz.height()) / 2;
+
+ m_clear->move(x, y);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/finddialog.cpp b/libs/uibase/src/finddialog.cpp
new file mode 100644
index 0000000..9284014
--- /dev/null
+++ b/libs/uibase/src/finddialog.cpp
@@ -0,0 +1,51 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/finddialog.h>
+#include "ui_finddialog.h"
+
+namespace MOBase
+{
+
+FindDialog::FindDialog(QWidget* parent) : QDialog(parent), ui(new Ui::FindDialog)
+{
+ ui->setupUi(this);
+}
+
+FindDialog::~FindDialog()
+{
+ delete ui;
+}
+
+void FindDialog::on_nextBtn_clicked()
+{
+ emit findNext();
+}
+
+void FindDialog::on_patternEdit_textChanged(const QString& pattern)
+{
+ emit patternChanged(pattern);
+}
+
+void FindDialog::on_closeBtn_clicked()
+{
+ this->close();
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/finddialog.ui b/libs/uibase/src/finddialog.ui
new file mode 100644
index 0000000..b31145d
--- /dev/null
+++ b/libs/uibase/src/finddialog.ui
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FindDialog</class>
+ <widget class="QDialog" name="FindDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>72</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Find</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Find what:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="patternEdit">
+ <property name="toolTip">
+ <string>Search term</string>
+ </property>
+ <property name="whatsThis">
+ <string>Search term</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QPushButton" name="nextBtn">
+ <property name="toolTip">
+ <string>Find next occurence from current file position.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Find next occurence from current file position.</string>
+ </property>
+ <property name="text">
+ <string>&amp;Find Next</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="closeBtn">
+ <property name="toolTip">
+ <string>Close</string>
+ </property>
+ <property name="whatsThis">
+ <string>Close</string>
+ </property>
+ <property name="text">
+ <string>Close</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/uibase/src/guessedvalue.cpp b/libs/uibase/src/guessedvalue.cpp
new file mode 100644
index 0000000..c8393af
--- /dev/null
+++ b/libs/uibase/src/guessedvalue.cpp
@@ -0,0 +1 @@
+#include <uibase/guessedvalue.h>
diff --git a/libs/uibase/src/ifiletree.cpp b/libs/uibase/src/ifiletree.cpp
new file mode 100644
index 0000000..b582d18
--- /dev/null
+++ b/libs/uibase/src/ifiletree.cpp
@@ -0,0 +1,1029 @@
+#include <uibase/ifiletree.h>
+
+#include <algorithm>
+#include <ranges>
+#include <span>
+#include <stack>
+
+#include <QRegularExpression>
+
+// FileTreeEntry:
+namespace MOBase
+{
+FileTreeEntry::FileTreeEntry(std::shared_ptr<const IFileTree> parent, QString name)
+ : m_Parent(parent), m_Name(name)
+{}
+
+QString FileTreeEntry::suffix() const
+{
+ const qsizetype idx = m_Name.lastIndexOf(".");
+ return (isDir() || idx == -1) ? "" : m_Name.mid(idx + 1);
+}
+
+bool FileTreeEntry::hasSuffix(QString suffix) const
+{
+ return this->suffix().compare(suffix, FileNameComparator::CaseSensitivity) == 0;
+}
+
+bool FileTreeEntry::hasSuffix(QStringList suffixes) const
+{
+ return suffixes.contains(suffix(), FileNameComparator::CaseSensitivity);
+}
+
+QString FileTreeEntry::pathFrom(std::shared_ptr<const IFileTree> tree,
+ QString sep) const
+{
+
+ // We will construct the path from right to left:
+ QString path = name();
+
+ auto p = parent();
+ while (p != nullptr && p != tree) {
+ // We need to check the parent, otherwize we are going to prepend the name
+ // and a / for the base, which we do not want.
+ if (p->parent() != nullptr) {
+ path = p->name() + sep + path;
+ }
+ p = p->parent();
+ }
+
+ return p == tree ? path : QString();
+}
+
+bool FileTreeEntry::detach()
+{
+ auto p = parent();
+ if (p == nullptr) {
+ return false;
+ }
+ return p->erase(shared_from_this()) != p->end();
+}
+
+bool FileTreeEntry::moveTo(std::shared_ptr<IFileTree> tree)
+{
+ return tree->insert(shared_from_this()) != tree->end();
+}
+
+std::shared_ptr<FileTreeEntry> FileTreeEntry::clone() const
+{
+ return createFileEntry(nullptr, name());
+}
+
+std::shared_ptr<FileTreeEntry>
+FileTreeEntry::createFileEntry(std::shared_ptr<const IFileTree> parent, QString name)
+{
+ return std::shared_ptr<FileTreeEntry>(new FileTreeEntry(parent, name));
+}
+} // namespace MOBase
+
+// IFileTree:
+namespace MOBase
+{
+
+/**
+ * Comparator for file entries.
+ */
+struct FileEntryComparator
+{
+ bool operator()(std::shared_ptr<FileTreeEntry> const& a,
+ std::shared_ptr<FileTreeEntry> const& b) const
+ {
+ if (a->isDir() && !b->isDir()) {
+ return true;
+ } else if (!a->isDir() && b->isDir()) {
+ return false;
+ } else {
+ return FileNameComparator::compare(a->name(), b->name()) < 0;
+ }
+ }
+};
+
+/**
+ * @brief Comparator that can be used to find entry matching the given name and
+ * file type.
+ */
+struct MatchEntryComparator
+{
+
+ MatchEntryComparator(QString const& name, FileTreeEntry::FileTypes matchTypes)
+ : m_Name(name), m_MatchTypes(matchTypes)
+ {}
+
+ bool operator()(const std::shared_ptr<const FileTreeEntry>& fileEntry) const
+ {
+ return m_MatchTypes.testFlag(fileEntry->fileType()) &&
+ fileEntry->compare(m_Name) == 0;
+ }
+
+ bool operator()(const std::shared_ptr<FileTreeEntry>& fileEntry) const
+ {
+ return m_MatchTypes.testFlag(fileEntry->fileType()) &&
+ fileEntry->compare(m_Name) == 0;
+ }
+
+private:
+ QString const& m_Name;
+ FileTreeEntry::FileTypes m_MatchTypes;
+};
+
+/**
+ *
+ */
+bool IFileTree::exists(QString path, FileTypes type) const
+{
+ return find(path, type) != nullptr;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::find(QString path, FileTypes type)
+{
+ return fetchEntry(splitPath(path), type);
+}
+std::shared_ptr<const FileTreeEntry> IFileTree::find(QString path, FileTypes type) const
+{
+ return fetchEntry(splitPath(path), type);
+}
+
+/**
+ *
+ */
+void IFileTree::walk(
+ std::function<WalkReturn(QString const&, std::shared_ptr<const FileTreeEntry>)>
+ callback,
+ QString sep) const
+{
+
+ std::stack<std::pair<QString, std::shared_ptr<const FileTreeEntry>>> stack;
+
+ // We start by pushing all the entries in this tree, this avoid having to do extra
+ // check later for avoid leading separator:
+ for (auto rit = rbegin(); rit != rend(); ++rit) {
+ stack.push({"", *rit});
+ }
+
+ while (!stack.empty()) {
+ auto [path, entry] = stack.top();
+ stack.pop();
+
+ auto res = callback(path, entry);
+ if (res == WalkReturn::STOP) {
+ break;
+ }
+ if (entry->isDir() && res != WalkReturn::SKIP) {
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ stack.push({path + tree->name() + sep, *rit});
+ }
+ }
+ }
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::addFile(QString path, bool replaceIfExists)
+{
+ QStringList parts = splitPath(path);
+
+ // Check if the file already exists:
+ auto existingEntry = fetchEntry(parts, IFileTree::FILE_OR_DIRECTORY);
+ if (!replaceIfExists && existingEntry != nullptr) {
+ return nullptr;
+ }
+
+ // Find or create the tree:
+ IFileTree* tree;
+
+ if (parts.size() > 1) {
+
+ // Create the tree:
+ std::shared_ptr<FileTreeEntry> treeEntry =
+ createTree(parts.begin(), parts.end() - 1);
+
+ // Early fail if the tree was not created:
+ if (treeEntry == nullptr) {
+ return nullptr;
+ }
+
+ tree = treeEntry->astree().get();
+ } else {
+ tree = this;
+ }
+
+ std::shared_ptr<FileTreeEntry> entry =
+ tree->makeFile(tree->astree(), parts[parts.size() - 1]);
+
+ // If makeFile returns a null pointer, it means we cannot create file:
+ if (entry == nullptr) {
+ return nullptr;
+ }
+
+ // Remove the existing files if there was one:
+ if (existingEntry) {
+ existingEntry->detach();
+ }
+
+ // Insert in the tree:
+ tree->entries().insert(
+ std::upper_bound(tree->begin(), tree->end(), entry, FileEntryComparator{}),
+ entry);
+
+ return entry;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::addDirectory(QString path)
+{
+ QStringList parts = splitPath(path);
+ return createTree(parts.begin(), parts.end());
+}
+
+/**
+ *
+ */
+IFileTree::iterator IFileTree::insert(std::shared_ptr<FileTreeEntry> entry,
+ InsertPolicy insertPolicy)
+{
+
+ // Check that this is not the current tree or a parent tree:
+ if (entry->isDir()) {
+ std::shared_ptr<IFileTree> tmp = astree();
+ while (tmp != nullptr) {
+ if (tmp == entry->astree()) {
+ return end();
+ }
+ tmp = tmp->parent();
+ }
+ }
+
+ // Check if there exists an entry with the same name:
+ auto existingIt = std::find_if(
+ begin(), end(), MatchEntryComparator{entry->name(), FILE_OR_DIRECTORY});
+
+ // Already in the tree?
+ if (existingIt != end() && *existingIt == entry) {
+ return existingIt;
+ }
+
+ auto insertionIt = end();
+
+ if (existingIt != end()) {
+ if (insertPolicy == InsertPolicy::FAIL_IF_EXISTS) {
+ return end();
+ }
+
+ // We replace if the policy is REPLACE or if the new and old entry are
+ // both files:
+ if (insertPolicy == InsertPolicy::REPLACE ||
+ ((*existingIt)->isFile() && entry->isFile())) {
+ if (beforeReplace(this, existingIt->get(), entry.get())) {
+ // Detach the old entry from its parent (not using .detach()
+ // to remove the entry since we are replacing it):
+ (*existingIt)->m_Parent.reset();
+ entries().erase(existingIt);
+
+ // insert at the right place
+ insertionIt = entries().insert(
+ std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry);
+ } else {
+ return end();
+ }
+ } else if ((*existingIt)->isFile() || entry->isFile()) {
+ // If we arrive here and one of the entry is a file, we fail:
+ return end();
+ } else {
+ // If we end up here, we know that the policy is MERGE and that both
+ // are directory that can be merged:
+ mergeTree((*existingIt)->astree(), entry->astree(), nullptr);
+ insertionIt = existingIt;
+ }
+ } else if (beforeInsert(this, entry.get())) {
+ insertionIt = entries().insert(
+ std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry);
+ }
+
+ // Remove the tree from its parent (parent() can be null if we are inserting
+ // a new tree):
+ if (entry->parent() != nullptr) {
+ entry->parent()->erase(entry);
+ }
+
+ // If the entry was actually inserted, we update its parent:
+ if (*insertionIt == entry) {
+ entry->m_Parent = astree();
+ }
+ // Otherwize, we reset it (if this was a merge operation):
+ else {
+ entry->m_Parent.reset();
+ }
+
+ return insertionIt;
+}
+
+/**
+ *
+ */
+bool IFileTree::move(std::shared_ptr<FileTreeEntry> entry, QString path,
+ InsertPolicy insertPolicy)
+{
+
+ // Check that this is not a parent tree:
+ if (entry->isDir()) {
+ std::shared_ptr<IFileTree> tmp = parent();
+ while (tmp != nullptr) {
+ if (tmp == entry->astree()) {
+ return false;
+ }
+ tmp = tmp->parent();
+ }
+ }
+
+ // Insert in folder or replace:
+ const bool insertFolder = path.isEmpty() || path.endsWith("/") || path.endsWith("\\");
+
+ // Retrieve the path:
+ QStringList parts = splitPath(path);
+
+ // Backup the entry name (in case the insertion fails), and update the
+ // name:
+ QString entryName = entry->m_Name;
+ if (!insertFolder) {
+ entry->m_Name = parts.takeLast();
+ }
+
+ // Find or create the tree:
+ IFileTree* tree;
+
+ if (!parts.isEmpty()) {
+
+ // Create the tree:
+ std::shared_ptr<FileTreeEntry> treeEntry = createTree(parts.begin(), parts.end());
+
+ // Early fail if the tree was not created:
+ if (treeEntry == nullptr) {
+ return false;
+ }
+
+ tree = treeEntry->astree().get();
+ } else {
+ tree = this;
+ }
+
+ // We try to insert, and if it fails we need to reset the name:
+ auto it = tree->insert(entry, insertPolicy);
+ if (it == tree->end()) {
+ entry->m_Name = entryName;
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry>
+IFileTree::copy(std::shared_ptr<const FileTreeEntry> entry, QString path,
+ InsertPolicy insertPolicy)
+{
+ // Note: If a conflict exists, the tree is cloned before checking the conflict, so
+ // this is not the most efficient way but copying tree should be pretty rare (and
+ // probably avoided anyway), and this allow us to use `move()` to do all the complex
+ // operations.
+ auto clone = entry->clone();
+ if (move(clone, path, insertPolicy)) {
+ return clone;
+ }
+ return nullptr;
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::merge(std::shared_ptr<IFileTree> source,
+ OverwritesType* overwrites)
+{
+
+ // Check that this is not a parent tree:
+ std::shared_ptr<IFileTree> tmp = astree();
+ while (tmp != nullptr) {
+ if (tmp == source) {
+ return MERGE_FAILED;
+ }
+ tmp = tmp->parent();
+ }
+
+ return mergeTree(astree(), source, overwrites);
+}
+
+/**
+ *
+ */
+IFileTree::iterator IFileTree::erase(std::shared_ptr<FileTreeEntry> entry)
+{
+
+ if (!beforeRemove(this, entry.get())) {
+ return end();
+ }
+
+ auto it = std::find(begin(), end(), entry);
+ if (it == end()) {
+ return it;
+ }
+ entry->m_Parent.reset();
+ return entries().erase(it);
+ ;
+}
+
+/**
+ *
+ */
+std::pair<IFileTree::iterator, std::shared_ptr<FileTreeEntry>>
+IFileTree::erase(QString name)
+{
+ auto it = std::find_if(begin(), end(), [&name](const auto& entry) {
+ return entry->compare(name) == 0;
+ });
+
+ if (it == end()) {
+ return {it, nullptr};
+ }
+
+ if (!beforeRemove(this, it->get())) {
+ return {end(), nullptr};
+ }
+
+ // Save the entry to return it:
+ auto entry = *it;
+ entry->m_Parent.reset();
+
+ return {entries().erase(it), entry};
+}
+
+/**
+ *
+ */
+bool IFileTree::clear()
+{
+ // Need to find the iterator up to which we should erase:
+ auto& entries_ = entries();
+ auto it = entries_.begin();
+ for (; it != entries_.end() && beforeRemove(this, it->get()); ++it) {
+ // Detach (but not remove from the vector):
+ (*it)->m_Parent.reset();
+ }
+ entries_.erase(entries_.begin(), it);
+ return empty();
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::removeAll(QStringList names)
+{
+ return removeIf([&names](auto& entry) {
+ return names.contains(entry->name(), Qt::CaseInsensitive);
+ });
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::removeIf(
+ std::function<bool(std::shared_ptr<FileTreeEntry> const&)> predicate)
+{
+ std::size_t osize = size();
+ auto& en = entries();
+ // Cannot use begin() and end() directly because those are immutable iterators:
+ en.erase(std::remove_if(en.begin(), en.end(),
+ [this, &predicate](auto& entry) {
+ return beforeRemove(this, entry.get()) && predicate(entry);
+ }),
+ en.end());
+ return osize - size();
+}
+
+/**
+ *
+ */
+QStringList IFileTree::splitPath(QString path)
+{
+ // Using raw \\ instead of QDir::separator() since we are replacing by /
+ // anyway, and this avoid pulling an extra header (like QDir) only
+ // for the separator.
+ return path.replace("\\", "/").split("/", Qt::SkipEmptyParts);
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeReplace(IFileTree const*, FileTreeEntry const*,
+ FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeInsert(IFileTree const*, FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+bool IFileTree::beforeRemove(IFileTree const*, FileTreeEntry const*)
+{
+ return true;
+}
+
+/**
+ *
+ */
+std::size_t IFileTree::mergeTree(std::shared_ptr<IFileTree> destination,
+ std::shared_ptr<IFileTree> source,
+ OverwritesType* overwrites)
+{
+
+ const auto comp = FileEntryComparator{};
+
+ // Number of overwritten entries:
+ std::size_t noverwrites = 0;
+
+ // Note: Using the iterators from the vector directly since the other ones
+ // cannot be assigned to.
+ auto &dstEntries = destination->entries(), &srcEntries = source->entries();
+
+ // Note: Since entries are not sorted by name but by type (file/directory)
+ // then name, there is no fast way to check for conflict.
+ for (auto& srcEntry : srcEntries) {
+
+ // Try to find an exact match (name and type) - This iterator also
+ // serve to know where the entry should be inserted:
+ auto dstIt = std::lower_bound(dstEntries.begin(), dstEntries.end(), srcEntry, comp);
+
+ // Exact match found:
+ if (dstIt != dstEntries.end() && (*dstIt)->compare(srcEntry->name()) == 0 &&
+ (*dstIt)->isFile() == srcEntry->isFile()) {
+
+ // Both directory, we merge:
+ if ((*dstIt)->isDir() && srcEntry->isDir()) {
+ noverwrites += mergeTree((*dstIt)->astree(), srcEntry->astree(), overwrites);
+
+ // Detach the entry:
+ srcEntry->m_Parent.reset();
+ }
+ // Otherwize, check if the source can replace the destination:
+ else if (beforeReplace(destination.get(), dstIt->get(), srcEntry.get())) {
+ // Remove the parent:
+ auto dstEntry = *dstIt;
+ dstEntry->m_Parent.reset();
+
+ // Update overwrites information:
+ noverwrites++;
+ if (overwrites != nullptr) {
+ overwrites->insert({dstEntry, srcEntry});
+ }
+
+ // Replace the destination:
+ *dstIt = srcEntry;
+ srcEntry->m_Parent = destination;
+ }
+ // If not, fails:
+ else {
+ return MERGE_FAILED;
+ }
+ } else {
+ // If we did not find a match, the only way to check is to look
+ // through the vector:
+ auto conflictIt = std::find_if(dstEntries.begin(), dstEntries.end(),
+ [name = srcEntry->name()](auto const& dstEntry) {
+ return dstEntry->compare(name) == 0;
+ });
+
+ // Conflict (note that here both entries are of different types, so no need to
+ // check if we replace or merge):
+ int deleteIndex = -1;
+ if (conflictIt != dstEntries.end()) {
+
+ // We check if we can replace the entry:
+ if (!beforeReplace(destination.get(), conflictIt->get(), srcEntry.get())) {
+ return MERGE_FAILED;
+ }
+
+ // We need to store the index because the insert() will mess up the
+ // iterators:
+ deleteIndex = static_cast<int>(conflictIt - std::begin(dstEntries));
+ if (dstIt < conflictIt) {
+ deleteIndex += 1;
+ }
+
+ // Detach the conflicting entry (we erase it later, after the insertion):
+ (*conflictIt)->m_Parent.reset();
+
+ // Update overwrites information:
+ noverwrites++;
+ if (overwrites != nullptr) {
+ overwrites->insert({*conflictIt, srcEntry});
+ }
+ }
+ // No conflict, we still have to check if we can insert:
+ else if (!beforeInsert(destination.get(), srcEntry.get())) {
+ return MERGE_FAILED;
+ }
+
+ // Insert the entry using the previous iterator:
+ dstEntries.insert(dstIt, srcEntry);
+
+ // We delete here:
+ if (deleteIndex != -1) {
+ dstEntries.erase(dstEntries.begin() + deleteIndex);
+ }
+
+ // Update the parent:
+ srcEntry->m_Parent = destination;
+ }
+ }
+
+ // Clear the sources:
+ srcEntries.clear();
+
+ return noverwrites;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::createOrphanTree(QString name) const
+{
+ auto directory = makeDirectory(nullptr, name);
+ if (directory != nullptr) {
+ directory->m_Populated = true;
+ }
+ return directory;
+}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::fetchEntry(QStringList path,
+ FileTypes matchTypes)
+{
+ return std::const_pointer_cast<FileTreeEntry>(
+ const_cast<const IFileTree*>(this)->fetchEntry(path, matchTypes));
+}
+std::shared_ptr<const FileTreeEntry> IFileTree::fetchEntry(QStringList const& path,
+ FileTypes matchTypes) const
+{
+ // Check to ensure that the path contains at least one element:
+ if (path.isEmpty()) {
+ return nullptr;
+ }
+
+ // Early check:
+ if (path[path.size() - 1].startsWith("*")) {
+ return nullptr;
+ }
+
+ const IFileTree* tree = this;
+ auto it = std::begin(path);
+ for (; tree != nullptr && it != std::end(path) - 1; ++it) {
+ // Special cases:
+ if (*it == ".") {
+ continue;
+ } else if (*it == "..") {
+ tree = tree->parent().get();
+ } else {
+ // Find the entry at the current level:
+ auto entryIt = std::find_if(tree->begin(), tree->end(),
+ MatchEntryComparator{*it, IFileTree::DIRECTORY});
+
+ // Early exists if the entry does not exist or is not a directory:
+ if (entryIt == tree->end()) {
+ tree = nullptr;
+ } else {
+ tree = (*entryIt)->astree().get();
+ }
+ }
+ }
+
+ if (tree == nullptr) {
+ return nullptr;
+ }
+
+ // We have the final tree:
+ auto entryIt =
+ std::find_if(tree->begin(), tree->end(), MatchEntryComparator{*it, matchTypes});
+ auto bIt = tree->end();
+ return entryIt == bIt ? nullptr : *entryIt;
+}
+
+/**
+ * @brief Create a new file under this tree.
+ *
+ * @param parent The current tree, without const-qualification.
+ * @param name Name of the file.
+ * @param time Modification time of the file.
+ *
+ * @return the created file.
+ */
+std::shared_ptr<FileTreeEntry>
+IFileTree::makeFile(std::shared_ptr<const IFileTree> parent, QString name) const
+{
+ return createFileEntry(parent, name);
+}
+
+/**
+ *
+ */
+IFileTree::IFileTree() {}
+
+/**
+ *
+ */
+std::shared_ptr<FileTreeEntry> IFileTree::clone() const
+{
+ std::shared_ptr<IFileTree> tree = doClone();
+
+ // Don't copy not populated tree, it is not useful:
+ if (m_Populated) {
+ tree->m_Populated = true;
+ auto& tentries = tree->m_Entries;
+ for (auto e : entries()) {
+ auto ce = e->clone();
+ ce->m_Parent = tree;
+ tentries.push_back(ce);
+ }
+ }
+
+ return tree;
+}
+
+/**
+ *
+ */
+std::shared_ptr<IFileTree> IFileTree::createTree(QStringList::const_iterator begin,
+ QStringList::const_iterator end)
+{
+ // The current tree and entry:
+ std::shared_ptr<IFileTree> tree = astree();
+ for (auto it = begin; tree != nullptr && it != end; ++it) {
+ // Special cases:
+ if (*it == ".") {
+ continue;
+ } else if (*it == "..") {
+ // parent() returns nullptr if it does not exist, so no
+ // check required:
+ tree = parent();
+ } else {
+
+ // Check if the entry exists (looking for both files and directories
+ // because we don't want to override a file):
+ auto entryIt =
+ std::find_if(tree->begin(), tree->end(),
+ MatchEntryComparator{*it, IFileTree::FILE_OR_DIRECTORY});
+
+ // Create if it does not:
+ if (entryIt == tree->end()) {
+ auto newTree = tree->makeDirectory(tree, *it);
+
+ // If makeDirectory returns a null pointer, it means we cannot create tree.
+ if (newTree == nullptr) {
+ tree = nullptr;
+ break;
+ }
+
+ // The tree is empty so already populated:
+ newTree->m_Populated = true;
+
+ tree->entries().insert(std::upper_bound(tree->begin(), tree->end(), newTree,
+ FileEntryComparator{}),
+ newTree);
+ tree = newTree;
+ } else if ((*entryIt)->isDir()) {
+ tree = (*entryIt)->astree();
+ } else { // Cannot go further:
+ tree = nullptr;
+ }
+ }
+ }
+
+ return tree;
+}
+
+/**
+ * @brief Retrieve the vector of entries after populating it if required.
+ *
+ * @return the vector of entries.
+ */
+std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries()
+{
+ std::call_once(m_OnceFlag, [this]() {
+ populate();
+ });
+ return m_Entries;
+}
+const std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries() const
+{
+ std::call_once(m_OnceFlag, [this]() {
+ populate();
+ });
+ return m_Entries;
+}
+
+/**
+ * @brief Populate the internal vectors and update the flag.
+ */
+void IFileTree::populate() const
+{
+ // Need to check m_Populated again here since the tree can be populated without
+ // a call to entries() (e.g., on copy/orphanTree):
+ if (!m_Populated) {
+ if (!doPopulate(astree(), m_Entries)) {
+ std::sort(std::begin(m_Entries), std::end(m_Entries), FileEntryComparator{});
+ }
+ m_Populated = true;
+ }
+}
+
+// walk and glob with generator
+
+std::generator<std::shared_ptr<const FileTreeEntry>>
+walk(std::shared_ptr<const IFileTree> fileTree)
+{
+ std::stack<std::shared_ptr<const FileTreeEntry>> stack;
+
+ // we start by pushing all the entries in this tree, this avoid having to do extra
+ // check later to avoid leading separator
+ for (auto rit = fileTree->rbegin(); rit != fileTree->rend(); ++rit) {
+ stack.push(*rit);
+ }
+
+ while (!stack.empty()) {
+ auto entry = stack.top();
+ stack.pop();
+
+ co_yield entry;
+
+ if (entry->isDir()) {
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ stack.push(*rit);
+ }
+ }
+ }
+}
+
+namespace
+{
+ using glob_stack_t = std::stack<
+ std::pair<std::shared_ptr<const FileTreeEntry>, std::span<QRegularExpression>>>;
+
+ // check the given entry against the list of patterns and add new (entry, patterns) to
+ // the given stack
+ //
+ std::generator<std::shared_ptr<const FileTreeEntry>>
+ ifiletree_glob_impl_step(glob_stack_t& glob_stack,
+ std::shared_ptr<const FileTreeEntry> entry,
+ std::span<QRegularExpression> const& patterns)
+ {
+ // no more patterns, nothing to do
+ if (patterns.size() == 0) {
+ co_return;
+ }
+
+ // special handling if the pattern is empty (correspond to a '**' glob)
+ if (patterns[0].pattern() == "") {
+
+ // if there are more patterns after '**', we need to check the entry again, e.g.,
+ // if the entry name is 'x' and the pattern is '**/x' to match it
+ if (patterns.size() != 1) {
+ glob_stack.emplace(entry, patterns.subspan(1));
+ }
+
+ // if the entry is a file, there is nothing to do with '**'
+ if (entry->isFile()) {
+ co_return;
+ }
+
+ // if this is the end of the patterns list, we need to yield the current entry
+ // since it is a directory
+ if (patterns.size() == 1) {
+ co_yield entry;
+ }
+
+ // recurse over childs, but for directories, we need to keep the leading '**' in
+ // the list of patterns since '**' can match multiple level of directories
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ glob_stack.emplace(*rit, (*rit)->isDir() ? patterns : patterns.subspan(1));
+ }
+ }
+ // otherwise (if the first patterns is not '**'), we simply check if we have a match
+ else if (patterns[0].match(entry->name()).hasMatch()) {
+ // this was the last pattern and we have a match, so we yield the current entry,
+ // not that this will yield intermediate matching directory, but this is expected
+ if (patterns.size() == 1) {
+ co_yield entry;
+ }
+
+ // if the entry is not a directory, we have nothing more to do
+ if (entry->isFile()) {
+ co_return;
+ }
+
+ // if all that remain after this pattern is a '**', we need to yield the current
+ // entry since '**' can also match an empty succession of directories, e.g. 'a/b'
+ // is matched by 'a/b/**'
+ if (patterns.size() == 2 && patterns[1].pattern() == "") {
+ co_yield entry;
+ }
+
+ // we then need to recurse over
+ auto tree = entry->astree();
+ for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) {
+ glob_stack.emplace(*rit, patterns.subspan(1));
+ }
+ }
+ }
+
+ // main function for IFileTree::glob - this function simply manages the stack of of
+ // entry / patterns to handle, and then falls back to ifiletree_glob_impl_step
+ //
+ std::generator<std::shared_ptr<const FileTreeEntry>>
+ ifiletree_glob_impl(std::shared_ptr<const FileTreeEntry> entry,
+ std::span<QRegularExpression> const& patterns)
+ {
+ glob_stack_t stack;
+ stack.emplace(entry, patterns);
+
+ while (!stack.empty()) {
+ const auto [childEntry, childPatterns] = stack.top();
+ stack.pop();
+ co_yield std::ranges::elements_of(
+ ifiletree_glob_impl_step(stack, childEntry, childPatterns));
+ }
+ }
+
+} // namespace
+
+/**
+ *
+ */
+std::generator<std::shared_ptr<const FileTreeEntry>>
+glob(std::shared_ptr<const IFileTree> fileTree, QString pattern,
+ GlobPatternType patternType)
+{
+ // replace \\ by / to simply handling
+ pattern = pattern.trimmed().replace("\\", "/");
+
+ // reduce successions of **/** to **, this makes it easier to handle it in the
+ // actual implementation
+ pattern = pattern.replace(QRegularExpression("(\\*\\*/)*\\*\\*"), "**");
+
+ // we are going to match directly starting from the child of the tree, so we need
+ // to handle the root here
+ //
+ // note: this is the only pattern that can match the tree itself
+ if (pattern == "**") {
+ co_yield fileTree;
+ }
+
+ // split pattern into blocks, we keep
+ const auto regexOptions = FileNameComparator::CaseSensitivity == Qt::CaseInsensitive
+ ? QRegularExpression::CaseInsensitiveOption
+ : QRegularExpression::NoPatternOption;
+ std::vector<QRegularExpression> patterns;
+ for (const auto& part : pattern.split("/")) {
+ if (part == "**") {
+ // for '**' pattern, push an empty regex
+ patterns.emplace_back();
+ } else {
+ const auto regexpPattern =
+ patternType == GlobPatternType::GLOB
+ ? QRegularExpression::wildcardToRegularExpression(
+ part, QRegularExpression::NonPathWildcardConversion)
+ : part;
+ const QRegularExpression regex(regexpPattern, regexOptions);
+
+ if (!regex.isValid()) {
+ throw InvalidGlobPatternException(regex.errorString());
+ }
+
+ patterns.push_back(regex);
+ }
+ }
+
+ // fall back to the main glob function for the child of this tree
+ for (const auto& child : *fileTree) {
+ co_yield std::ranges::elements_of(ifiletree_glob_impl(child, patterns));
+ }
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/imodrepositorybridge.cpp b/libs/uibase/src/imodrepositorybridge.cpp
new file mode 100644
index 0000000..9b610b0
--- /dev/null
+++ b/libs/uibase/src/imodrepositorybridge.cpp
@@ -0,0 +1 @@
+#include <uibase/imodrepositorybridge.h>
diff --git a/libs/uibase/src/imoinfo.cpp b/libs/uibase/src/imoinfo.cpp
new file mode 100644
index 0000000..faa2db9
--- /dev/null
+++ b/libs/uibase/src/imoinfo.cpp
@@ -0,0 +1,23 @@
+#include <uibase/imoinfo.h>
+
+namespace MOBase
+{
+
+static QString g_pluginDataPath;
+
+QString IOrganizer::getPluginDataPath()
+{
+ return g_pluginDataPath;
+}
+
+} // namespace MOBase
+
+namespace MOBase::details
+{
+
+void setPluginDataPath(const QString& s)
+{
+ g_pluginDataPath = s;
+}
+
+} // namespace MOBase::details
diff --git a/libs/uibase/src/json.cpp b/libs/uibase/src/json.cpp
new file mode 100644
index 0000000..49d5fb3
--- /dev/null
+++ b/libs/uibase/src/json.cpp
@@ -0,0 +1,557 @@
+/**
+ * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and
+ * vice-versa. Copyright (C) 2011 Eeli Reilin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * \file json.cpp
+ */
+
+#include <uibase/json.h>
+
+namespace QtJson
+{
+static QString sanitizeString(QString str);
+static QByteArray join(const QList<QByteArray>& list, const QByteArray& sep);
+static QVariant parseValue(const QString& json, int& index, bool& success);
+static QVariant parseObject(const QString& json, int& index, bool& success);
+static QVariant parseArray(const QString& json, int& index, bool& success);
+static QVariant parseString(const QString& json, int& index, bool& success);
+static QVariant parseNumber(const QString& json, int& index);
+static int lastIndexOfNumber(const QString& json, int index);
+static void eatWhitespace(const QString& json, int& index);
+static int lookAhead(const QString& json, int index);
+static int nextToken(const QString& json, int& index);
+
+template <typename T>
+QByteArray serializeMap(const T& map, bool& success)
+{
+ QByteArray str = "{ ";
+ QList<QByteArray> pairs;
+ for (typename T::const_iterator it = map.begin(), itend = map.end(); it != itend;
+ ++it) {
+ QByteArray serializedValue = serialize(it.value());
+ if (serializedValue.isNull()) {
+ success = false;
+ break;
+ }
+ pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue;
+ }
+
+ str += join(pairs, ", ");
+ str += " }";
+ return str;
+}
+
+/**
+ * parse
+ */
+QVariant parse(const QString& json)
+{
+ bool success = true;
+ return parse(json, success);
+}
+
+/**
+ * parse
+ */
+QVariant parse(const QString& json, bool& success)
+{
+ success = true;
+
+ // Return an empty QVariant if the JSON data is either null or empty
+ if (!json.isNull() || !json.isEmpty()) {
+ QString data = json;
+ // We'll start from index 0
+ int index = 0;
+
+ // Parse the first value
+ QVariant value = parseValue(data, index, success);
+
+ // Return the parsed value
+ return value;
+ } else {
+ // Return the empty QVariant
+ return QVariant();
+ }
+}
+
+QByteArray serialize(const QVariant& data)
+{
+ bool success = true;
+ return serialize(data, success);
+}
+
+QByteArray serialize(const QVariant& data, bool& success)
+{
+ QByteArray str;
+ success = true;
+
+ if (!data.isValid()) { // invalid or null?
+ str = "null";
+ } else if ((data.typeId() == QMetaType::Type::QVariantList) ||
+ (data.typeId() == QMetaType::Type::QStringList)) { // variant is a list?
+ QList<QByteArray> values;
+ const QVariantList list = data.toList();
+ Q_FOREACH (const QVariant& v, list) {
+ QByteArray serializedValue = serialize(v);
+ if (serializedValue.isNull()) {
+ success = false;
+ break;
+ }
+ values << serializedValue;
+ }
+
+ str = "[ " + join(values, ", ") + " ]";
+ } else if (data.typeId() == QMetaType::Type::QVariantHash) { // variant is a hash?
+ str = serializeMap<>(data.toHash(), success);
+ } else if (data.typeId() == QMetaType::Type::QVariantMap) { // variant is a map?
+ str = serializeMap<>(data.toMap(), success);
+ } else if ((data.typeId() == QMetaType::Type::QString) ||
+ (data.typeId() ==
+ QMetaType::Type::QByteArray)) { // a string or a byte array?
+ str = sanitizeString(data.toString()).toUtf8();
+ } else if (data.typeId() == QMetaType::Type::Double) { // double?
+ double value = data.toDouble();
+ if ((value - value) == 0.0) {
+ str = QByteArray::number(value, 'g');
+ if (!str.contains(".") && !str.contains("e")) {
+ str += ".0";
+ }
+ } else {
+ success = false;
+ }
+ } else if (data.typeId() == QMetaType::Type::Bool) { // boolean value?
+ str = data.toBool() ? "true" : "false";
+ } else if (data.typeId() == QMetaType::Type::ULongLong) { // large unsigned number?
+ str = QByteArray::number(data.value<qulonglong>());
+ } else if (data.canConvert<qlonglong>()) { // any signed number?
+ str = QByteArray::number(data.value<qlonglong>());
+ } else if (data.canConvert<long>()) { // TODO: this code is never executed
+ str = QString::number(data.value<long>()).toUtf8();
+ } else if (data.canConvert<QString>()) { // can value be converted to string?
+ // this will catch QDate, QDateTime, QUrl, ...
+ str = sanitizeString(data.toString()).toUtf8();
+ } else {
+ success = false;
+ }
+
+ if (success) {
+ return str;
+ } else {
+ return QByteArray();
+ }
+}
+
+QString serializeStr(const QVariant& data)
+{
+ return QString::fromUtf8(serialize(data));
+}
+
+QString serializeStr(const QVariant& data, bool& success)
+{
+ return QString::fromUtf8(serialize(data, success));
+}
+
+/**
+ * \enum JsonToken
+ */
+enum JsonToken
+{
+ JsonTokenNone = 0,
+ JsonTokenCurlyOpen = 1,
+ JsonTokenCurlyClose = 2,
+ JsonTokenSquaredOpen = 3,
+ JsonTokenSquaredClose = 4,
+ JsonTokenColon = 5,
+ JsonTokenComma = 6,
+ JsonTokenString = 7,
+ JsonTokenNumber = 8,
+ JsonTokenTrue = 9,
+ JsonTokenFalse = 10,
+ JsonTokenNull = 11
+};
+
+static QString sanitizeString(QString str)
+{
+ str.replace(QLatin1String("\\"), QLatin1String("\\\\"));
+ str.replace(QLatin1String("\""), QLatin1String("\\\""));
+ str.replace(QLatin1String("\b"), QLatin1String("\\b"));
+ str.replace(QLatin1String("\f"), QLatin1String("\\f"));
+ str.replace(QLatin1String("\n"), QLatin1String("\\n"));
+ str.replace(QLatin1String("\r"), QLatin1String("\\r"));
+ str.replace(QLatin1String("\t"), QLatin1String("\\t"));
+ return QString(QLatin1String("\"%1\"")).arg(str);
+}
+
+static QByteArray join(const QList<QByteArray>& list, const QByteArray& sep)
+{
+ QByteArray res;
+ Q_FOREACH (const QByteArray& i, list) {
+ if (!res.isEmpty()) {
+ res += sep;
+ }
+ res += i;
+ }
+ return res;
+}
+
+/**
+ * parseValue
+ */
+static QVariant parseValue(const QString& json, int& index, bool& success)
+{
+ // Determine what kind of data we should parse by
+ // checking out the upcoming token
+ switch (lookAhead(json, index)) {
+ case JsonTokenString:
+ return parseString(json, index, success);
+ case JsonTokenNumber:
+ return parseNumber(json, index);
+ case JsonTokenCurlyOpen:
+ return parseObject(json, index, success);
+ case JsonTokenSquaredOpen:
+ return parseArray(json, index, success);
+ case JsonTokenTrue:
+ nextToken(json, index);
+ return QVariant(true);
+ case JsonTokenFalse:
+ nextToken(json, index);
+ return QVariant(false);
+ case JsonTokenNull:
+ nextToken(json, index);
+ return QVariant();
+ case JsonTokenNone:
+ break;
+ }
+
+ // If there were no tokens, flag the failure and return an empty QVariant
+ success = false;
+ return QVariant();
+}
+
+/**
+ * parseObject
+ */
+static QVariant parseObject(const QString& json, int& index, bool& success)
+{
+ QVariantMap map;
+ int token;
+
+ // Get rid of the whitespace and increment index
+ nextToken(json, index);
+
+ // Loop through all of the key/value pairs of the object
+ bool done = false;
+ while (!done) {
+ // Get the upcoming token
+ token = lookAhead(json, index);
+
+ if (token == JsonTokenNone) {
+ success = false;
+ return QVariantMap();
+ } else if (token == JsonTokenComma) {
+ nextToken(json, index);
+ } else if (token == JsonTokenCurlyClose) {
+ nextToken(json, index);
+ return map;
+ } else {
+ // Parse the key/value pair's name
+ QString name = parseString(json, index, success).toString();
+
+ if (!success) {
+ return QVariantMap();
+ }
+
+ // Get the next token
+ token = nextToken(json, index);
+
+ // If the next token is not a colon, flag the failure
+ // return an empty QVariant
+ if (token != JsonTokenColon) {
+ success = false;
+ return QVariant(QVariantMap());
+ }
+
+ // Parse the key/value pair's value
+ QVariant value = parseValue(json, index, success);
+
+ if (!success) {
+ return QVariantMap();
+ }
+
+ // Assign the value to the key in the map
+ map[name] = value;
+ }
+ }
+
+ // Return the map successfully
+ return QVariant(map);
+}
+
+/**
+ * parseArray
+ */
+static QVariant parseArray(const QString& json, int& index, bool& success)
+{
+ QVariantList list;
+
+ nextToken(json, index);
+
+ bool done = false;
+ while (!done) {
+ int token = lookAhead(json, index);
+
+ if (token == JsonTokenNone) {
+ success = false;
+ return QVariantList();
+ } else if (token == JsonTokenComma) {
+ nextToken(json, index);
+ } else if (token == JsonTokenSquaredClose) {
+ nextToken(json, index);
+ break;
+ } else {
+ QVariant value = parseValue(json, index, success);
+ if (!success) {
+ return QVariantList();
+ }
+ list.push_back(value);
+ }
+ }
+
+ return QVariant(list);
+}
+
+/**
+ * parseString
+ */
+static QVariant parseString(const QString& json, int& index, bool& success)
+{
+ QString s;
+ QChar c;
+
+ eatWhitespace(json, index);
+
+ c = json[index++];
+
+ bool complete = false;
+ while (!complete) {
+ if (index == json.size()) {
+ break;
+ }
+
+ c = json[index++];
+
+ if (c == '\"') {
+ complete = true;
+ break;
+ } else if (c == '\\') {
+ if (index == json.size()) {
+ break;
+ }
+
+ c = json[index++];
+
+ if (c == '\"') {
+ s.append('\"');
+ } else if (c == '\\') {
+ s.append('\\');
+ } else if (c == '/') {
+ s.append('/');
+ } else if (c == 'b') {
+ s.append('\b');
+ } else if (c == 'f') {
+ s.append('\f');
+ } else if (c == 'n') {
+ s.append('\n');
+ } else if (c == 'r') {
+ s.append('\r');
+ } else if (c == 't') {
+ s.append('\t');
+ } else if (c == 'u') {
+ qsizetype remainingLength = json.size() - index;
+ if (remainingLength >= 4) {
+ QString unicodeStr = json.mid(index, 4);
+
+ int symbol = unicodeStr.toInt(0, 16);
+
+ s.append(QChar(symbol));
+
+ index += 4;
+ } else {
+ break;
+ }
+ }
+ } else {
+ s.append(c);
+ }
+ }
+
+ if (!complete) {
+ success = false;
+ return QVariant();
+ }
+
+ return QVariant(s);
+}
+
+/**
+ * parseNumber
+ */
+static QVariant parseNumber(const QString& json, int& index)
+{
+ eatWhitespace(json, index);
+
+ int lastIndex = lastIndexOfNumber(json, index);
+ int charLength = (lastIndex - index) + 1;
+ QString numberStr;
+
+ numberStr = json.mid(index, charLength);
+
+ index = lastIndex + 1;
+ bool ok;
+
+ if (numberStr.contains('.')) {
+ return QVariant(numberStr.toDouble(nullptr));
+ } else if (numberStr.startsWith('-')) {
+ int i = numberStr.toInt(&ok);
+ if (!ok) {
+ qlonglong ll = numberStr.toLongLong(&ok);
+ return ok ? ll : QVariant(numberStr);
+ }
+ return i;
+ } else {
+ uint u = numberStr.toUInt(&ok);
+ if (!ok) {
+ qulonglong ull = numberStr.toULongLong(&ok);
+ return ok ? ull : QVariant(numberStr);
+ }
+ return u;
+ }
+}
+
+/**
+ * lastIndexOfNumber
+ */
+static int lastIndexOfNumber(const QString& json, int index)
+{
+ int lastIndex;
+
+ for (lastIndex = index; lastIndex < json.size(); lastIndex++) {
+ if (QString("0123456789+-.eE").indexOf(json[lastIndex]) == -1) {
+ break;
+ }
+ }
+
+ return lastIndex - 1;
+}
+
+/**
+ * eatWhitespace
+ */
+static void eatWhitespace(const QString& json, int& index)
+{
+ for (; index < json.size(); index++) {
+ if (QString(" \t\n\r").indexOf(json[index]) == -1) {
+ break;
+ }
+ }
+}
+
+/**
+ * lookAhead
+ */
+static int lookAhead(const QString& json, int index)
+{
+ int saveIndex = index;
+ return nextToken(json, saveIndex);
+}
+
+/**
+ * nextToken
+ */
+static int nextToken(const QString& json, int& index)
+{
+ eatWhitespace(json, index);
+
+ if (index == json.size()) {
+ return JsonTokenNone;
+ }
+
+ QChar c = json[index];
+ index++;
+ switch (c.toLatin1()) {
+ case '{':
+ return JsonTokenCurlyOpen;
+ case '}':
+ return JsonTokenCurlyClose;
+ case '[':
+ return JsonTokenSquaredOpen;
+ case ']':
+ return JsonTokenSquaredClose;
+ case ',':
+ return JsonTokenComma;
+ case '"':
+ return JsonTokenString;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '-':
+ return JsonTokenNumber;
+ case ':':
+ return JsonTokenColon;
+ }
+ index--; // ^ WTF?
+
+ qsizetype remainingLength = json.size() - index;
+
+ // True
+ if (remainingLength >= 4) {
+ if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' &&
+ json[index + 3] == 'e') {
+ index += 4;
+ return JsonTokenTrue;
+ }
+ }
+
+ // False
+ if (remainingLength >= 5) {
+ if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' &&
+ json[index + 3] == 's' && json[index + 4] == 'e') {
+ index += 5;
+ return JsonTokenFalse;
+ }
+ }
+
+ // Null
+ if (remainingLength >= 4) {
+ if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' &&
+ json[index + 3] == 'l') {
+ index += 4;
+ return JsonTokenNull;
+ }
+ }
+
+ return JsonTokenNone;
+}
+} // namespace QtJson
diff --git a/libs/uibase/src/lineeditclear.cpp b/libs/uibase/src/lineeditclear.cpp
new file mode 100644
index 0000000..476c874
--- /dev/null
+++ b/libs/uibase/src/lineeditclear.cpp
@@ -0,0 +1,50 @@
+/****************************************************************************
+**
+** Copyright (c) 2007 Trolltech ASA <info@trolltech.com>
+**
+** Use, modification and distribution is allowed without limitation,
+** warranty, liability or support of any kind.
+**
+****************************************************************************/
+
+#include <uibase/lineeditclear.h>
+#include <QStyle>
+#include <QToolButton>
+
+namespace MOBase
+{
+
+LineEditClear::LineEditClear(QWidget* parent) : QLineEdit(parent)
+{
+ clearButton = new QToolButton(this);
+ QPixmap pixmap(":/MO/gui/edit_clear");
+ clearButton->setIcon(QIcon(pixmap));
+ clearButton->setIconSize(pixmap.size());
+ clearButton->setCursor(Qt::ArrowCursor);
+ clearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }");
+ clearButton->hide();
+ connect(clearButton, SIGNAL(clicked()), this, SLOT(clear()));
+ connect(this, SIGNAL(textChanged(const QString&)), this,
+ SLOT(updateCloseButton(const QString&)));
+ int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
+ setStyleSheet(QString("QLineEdit { padding-right: %1px; } ")
+ .arg(clearButton->sizeHint().width() + frameWidth + 1));
+ /* QSize msz = minimumSizeHint();
+ setMinimumSize(qMax(msz.width(), clearButton->sizeHint().height() + frameWidth * 2 +
+ 2), qMax(msz.height(), clearButton->sizeHint().height() + frameWidth * 2 + 2));*/
+}
+
+void LineEditClear::resizeEvent(QResizeEvent*)
+{
+ QSize sz = clearButton->sizeHint();
+ int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
+ clearButton->move(rect().right() - frameWidth - sz.width(),
+ (rect().bottom() + 1 - sz.height()) / 2);
+}
+
+void LineEditClear::updateCloseButton(const QString& text)
+{
+ clearButton->setVisible(!text.isEmpty());
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/linklabel.cpp b/libs/uibase/src/linklabel.cpp
new file mode 100644
index 0000000..c4d09f1
--- /dev/null
+++ b/libs/uibase/src/linklabel.cpp
@@ -0,0 +1,27 @@
+#include <uibase/linklabel.h>
+#include <QApplication>
+
+QColor LinkLabel::m_linkColor;
+
+LinkLabel::LinkLabel(QWidget* parent) : QLabel(parent) {}
+
+QColor LinkLabel::linkColor() const
+{
+ return m_linkColor;
+}
+
+void LinkLabel::setLinkColor(const QColor& c)
+{
+ if (m_linkColor != c) {
+ m_linkColor = c;
+
+ // setting link color on the global palette; qt doesn't seem to support
+ // per-widget colors for links
+ if (qApp) {
+ auto p = qApp->palette();
+ p.setColor(QPalette::Link, c);
+ p.setColor(QPalette::LinkVisited, c);
+ qApp->setPalette(p);
+ }
+ }
+}
diff --git a/libs/uibase/src/log.cpp b/libs/uibase/src/log.cpp
new file mode 100644
index 0000000..a44c9e3
--- /dev/null
+++ b/libs/uibase/src/log.cpp
@@ -0,0 +1,411 @@
+#include <uibase/log.h>
+#include "pch.h"
+#include <uibase/utility.h>
+#include <iostream>
+
+#include <algorithm>
+#include <locale>
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4365)
+#endif
+
+#ifdef _WIN32
+#define SPDLOG_WCHAR_FILENAMES 1
+#endif
+
+#include <spdlog/logger.h>
+#include <spdlog/sinks/base_sink.h>
+#include <spdlog/sinks/basic_file_sink.h>
+#include <spdlog/sinks/daily_file_sink.h>
+#include <spdlog/sinks/dist_sink.h>
+#include <spdlog/sinks/rotating_file_sink.h>
+#include <spdlog/sinks/stdout_color_sinks.h>
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+namespace MOBase::log
+{
+
+namespace fs = std::filesystem;
+static std::unique_ptr<Logger> g_default;
+
+spdlog::level::level_enum toSpdlog(Levels lv)
+{
+ switch (lv) {
+ case Debug:
+ return spdlog::level::debug;
+
+ case Warning:
+ return spdlog::level::warn;
+
+ case Error:
+ return spdlog::level::err;
+
+ case Info: // fall-through
+ default:
+ return spdlog::level::info;
+ }
+}
+
+Levels fromSpdlog(spdlog::level::level_enum lv)
+{
+ switch (lv) {
+ case spdlog::level::trace:
+ case spdlog::level::debug:
+ return Debug;
+
+ case spdlog::level::warn:
+ return Warning;
+
+ case spdlog::level::critical: // fall-through
+ case spdlog::level::err:
+ return Error;
+
+ case spdlog::level::info: // fall-through
+ case spdlog::level::off:
+ case spdlog::level::n_levels: // to please MSVC
+ default:
+ return Info;
+ }
+}
+
+class CallbackSink : public spdlog::sinks::base_sink<std::mutex>
+{
+public:
+ CallbackSink(Callback* f) : m_f(f) {}
+
+ void setCallback(Callback* f) { m_f = f; }
+
+protected:
+ void sink_it_(const spdlog::details::log_msg& m) override
+ {
+ thread_local bool active = false;
+
+ if (active) {
+ // trying to log from a log callback, ignoring
+ return;
+ }
+
+ if (!m_f) {
+ // disabled
+ return;
+ }
+
+ try {
+ auto g = Guard([&] {
+ active = false;
+ });
+ active = true;
+
+ Entry e;
+ e.time = m.time;
+ e.level = fromSpdlog(m.level);
+ e.message = std::string(m.payload);
+
+ spdlog::memory_buf_t formatted;
+ base_sink::formatter_->format(m, formatted);
+
+ if (formatted.size() >= 2) {
+ // remove \r\n
+ e.formattedMessage.assign(formatted.begin(), formatted.end() - 2);
+ } else {
+ e.formattedMessage = std::string(formatted);
+ }
+
+ (*m_f)(std::move(e));
+ } catch (std::exception& e) {
+ fprintf(stderr, "uncaugh exception in logging callback, %s\n", e.what());
+ } catch (...) {
+ fprintf(stderr, "uncaught exception in logging callback\n");
+ }
+ }
+
+ void flush_() override
+ {
+ // no-op
+ }
+
+private:
+ std::atomic<Callback*> m_f;
+};
+
+File::File() : type(None), maxSize(0), maxFiles(0), dailyHour(0), dailyMinute(0) {}
+
+File File::daily(fs::path file, int hour, int minute)
+{
+ File fl;
+
+ fl.type = Daily;
+ fl.file = std::move(file);
+ fl.dailyHour = hour;
+ fl.dailyMinute = minute;
+
+ return fl;
+}
+
+File File::rotating(fs::path file, std::size_t maxSize, std::size_t maxFiles)
+{
+ File fl;
+
+ fl.type = Rotating;
+ fl.file = std::move(file);
+ fl.maxSize = maxSize;
+ fl.maxFiles = maxFiles;
+
+ return fl;
+}
+
+File File::single(std::filesystem::path file)
+{
+ File fl;
+
+ fl.type = Single;
+ fl.file = std::move(file);
+
+ return fl;
+}
+
+spdlog::sink_ptr createFileSink(const File& f)
+{
+ try {
+ switch (f.type) {
+ case File::Daily: {
+ return std::make_shared<spdlog::sinks::daily_file_sink_mt>(
+ f.file.native(), f.dailyHour, f.dailyMinute);
+ }
+
+ case File::Rotating: {
+ return std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
+ f.file.native(), f.maxSize, f.maxFiles);
+ }
+
+ case File::Single: {
+ return std::make_shared<spdlog::sinks::basic_file_sink_mt>(f.file.native(), true);
+ }
+
+ case File::None: // fall-through
+ default:
+ return {};
+ }
+ } catch (spdlog::spdlog_ex& e) {
+ std::cerr << "failed to create file log, " << e.what() << "\n";
+ return {};
+ }
+}
+
+Logger::Logger(LoggerConfiguration conf_moved) : m_conf(std::move(conf_moved))
+{
+ createLogger(m_conf.name);
+
+ const auto timeType =
+ m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local;
+
+ m_logger->set_level(toSpdlog(m_conf.maxLevel));
+ m_logger->set_pattern(m_conf.pattern, timeType);
+ m_logger->flush_on(spdlog::level::trace);
+}
+
+// anchor
+Logger::~Logger() = default;
+
+Levels Logger::level() const
+{
+ return fromSpdlog(m_logger->level());
+}
+
+void Logger::setLevel(Levels lv)
+{
+ m_logger->set_level(toSpdlog(lv));
+}
+
+void Logger::setPattern(const std::string& s)
+{
+ m_logger->set_pattern(s);
+}
+
+void Logger::setFile(const File& f)
+{
+
+ if (m_file) {
+ auto* ds = static_cast<spdlog::sinks::dist_sink<std::mutex>*>(m_sinks.get());
+ ds->remove_sink(m_file);
+ m_file = {};
+ }
+
+ if (f.type != File::None) {
+ try {
+ m_file = createFileSink(f);
+
+ if (m_file) {
+ addSink(m_file);
+ }
+ } catch (spdlog::spdlog_ex& e) {
+ error("{}", e.what());
+ }
+ }
+}
+
+void Logger::setCallback(Callback* f)
+{
+ if (m_callback) {
+ static_cast<CallbackSink*>(m_callback.get())->setCallback(f);
+ } else {
+ m_callback.reset(new CallbackSink(f));
+ addSink(m_callback);
+ }
+}
+
+void Logger::addToBlacklist(const std::string& filter, const std::string& replacement)
+{
+ if (filter.length() <= 0 || replacement.length() <= 0) {
+ // nothing to do
+ return;
+ }
+
+ bool present = false;
+ for (BlacklistEntry& e : m_conf.blacklist) {
+ if (iequals(e.filter, filter)) {
+ e.replacement = replacement;
+ present = true;
+ break;
+ }
+ }
+ if (!present) {
+ m_conf.blacklist.push_back(BlacklistEntry(filter, replacement));
+ }
+}
+
+void Logger::removeFromBlacklist(const std::string& filter)
+{
+ if (filter.length() <= 0) {
+ // nothing to do
+ return;
+ }
+
+ for (auto it = m_conf.blacklist.begin(); it != m_conf.blacklist.end();) {
+ if (iequals(it->filter, filter)) {
+ it = m_conf.blacklist.erase(it);
+ } else {
+ ++it;
+ }
+ }
+}
+
+void Logger::resetBlacklist()
+{
+ m_conf.blacklist.clear();
+}
+
+void Logger::createLogger(const std::string& name)
+{
+ m_sinks.reset(new spdlog::sinks::dist_sink<std::mutex>);
+
+#ifdef _WIN32
+ DWORD console_mode;
+ if (::GetConsoleMode(::GetStdHandle(STD_ERROR_HANDLE), &console_mode) != 0) {
+ using sink_type = spdlog::sinks::wincolor_stderr_sink_mt;
+ m_console.reset(new sink_type);
+
+ if (auto* cs = dynamic_cast<sink_type*>(m_console.get())) {
+ cs->set_color(spdlog::level::info,
+ FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
+ cs->set_color(spdlog::level::debug,
+ FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
+ }
+
+ addSink(m_console);
+ }
+#else
+ // On Linux, use ANSI color stderr sink
+ m_console.reset(new spdlog::sinks::ansicolor_stderr_sink_mt);
+ addSink(m_console);
+#endif
+
+ m_logger.reset(new spdlog::logger(name, m_sinks));
+}
+
+void Logger::addSink(std::shared_ptr<spdlog::sinks::sink> sink)
+{
+ // this is called for both the file and callback sinks
+ //
+ // in createLogger(), the dist_sink that was just created will be given the
+ // pattern that was set in Logger::Logger(), and will pass it to its children;
+ // the log level is irrelevant in child sinks because dist_sink checks it
+ // itself
+ //
+ // the problem then is that dist_sink doesn't have children yet, they're added
+ // in setFile() and setCallback(), which can be called by the user much later
+ // (or not at all)
+ //
+ // however, when a sink is added to dist_sink, it does _not_ set the pattern
+ // on it, it merely adds it to the list
+ //
+ // this sets the formatter on the sink manually before adding it to dist_sink
+
+ auto* ds = static_cast<spdlog::sinks::dist_sink<std::mutex>*>(m_sinks.get());
+
+ const auto timeType =
+ m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local;
+
+ sink->set_formatter(
+ std::make_unique<spdlog::pattern_formatter>(m_conf.pattern, timeType));
+
+ ds->add_sink(sink);
+}
+
+QString levelToString(Levels level)
+{
+ const auto spdlogLevel = toSpdlog(level);
+ const auto sv = spdlog::level::to_string_view(spdlogLevel);
+ const std::string s(sv.begin(), sv.end());
+
+ return QString::fromStdString(s);
+}
+
+void createDefault(LoggerConfiguration conf)
+{
+ g_default = std::make_unique<Logger>(conf);
+}
+
+Logger& getDefault()
+{
+ Q_ASSERT(g_default);
+ return *g_default;
+}
+
+} // namespace MOBase::log
+
+namespace MOBase::log::details
+{
+
+void doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept
+{
+ try {
+ const char* start = s.c_str();
+ const char* p = start;
+
+ for (;;) {
+ while (*p && *p != '\n') {
+ ++p;
+ }
+
+ std::string_view sv(start, static_cast<std::size_t>(p - start));
+ lg.log(toSpdlog(lv), "{}", sv);
+
+ if (!*p) {
+ break;
+ }
+
+ ++p;
+ start = p;
+ }
+ } catch (...) {
+ // eat it
+ }
+}
+
+} // namespace MOBase::log::details
diff --git a/libs/uibase/src/modrepositoryfileinfo.cpp b/libs/uibase/src/modrepositoryfileinfo.cpp
new file mode 100644
index 0000000..2b0f729
--- /dev/null
+++ b/libs/uibase/src/modrepositoryfileinfo.cpp
@@ -0,0 +1,81 @@
+#include <uibase/modrepositoryfileinfo.h>
+#include <uibase/json.h>
+
+MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo(
+ const ModRepositoryFileInfo& reference)
+ : QObject(reference.parent()), name(reference.name), uri(reference.uri),
+ description(reference.description), version(reference.version),
+ categoryID(reference.categoryID), modName(reference.modName),
+ gameName(reference.gameName), modID(reference.modID), fileID(reference.fileID),
+ fileSize(reference.fileSize), fileCategory(reference.fileCategory),
+ repository(reference.repository), userData(reference.userData),
+ author(reference.author), uploader(reference.uploader),
+ uploaderUrl(reference.uploaderUrl)
+{}
+
+MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo(QString gameName, int modID,
+ int fileID)
+ : name(), uri(), description(), version(), categoryID(0), modName(),
+ gameName(gameName), modID(modID), fileID(fileID), fileSize(0),
+ fileCategory(TYPE_UNKNOWN), repository(), userData(), author(), uploader(),
+ uploaderUrl()
+
+{}
+
+MOBase::ModRepositoryFileInfo
+MOBase::ModRepositoryFileInfo::createFromJson(const QString& data)
+{
+ QVariantList result = QtJson::parse(data).toList();
+
+ while (result.length() < 18) {
+ result.append(QVariant());
+ }
+
+ ModRepositoryFileInfo newInfo;
+
+ newInfo.gameName = result.at(0).toString();
+ newInfo.fileID = result.at(1).toInt();
+ newInfo.name = result.at(2).toString();
+ newInfo.uri = result.at(3).toString();
+ newInfo.version.parse(result.at(4).toString());
+ newInfo.description = result.at(5).toString();
+ newInfo.categoryID = result.at(6).toInt();
+ newInfo.fileSize = result.at(7).toUInt();
+ newInfo.modID = result.at(8).toInt();
+ newInfo.modName = result.at(9).toString();
+ newInfo.newestVersion.parse(result.at(10).toString());
+ newInfo.fileName = result.at(11).toString();
+ newInfo.fileCategory = result.at(12).toInt();
+ newInfo.repository = result.at(13).toString();
+ newInfo.userData = result.at(14).toMap();
+ newInfo.author = result.at(15).toString();
+ newInfo.uploader = result.at(16).toString();
+ newInfo.uploaderUrl = result.at(17).toString();
+
+ return newInfo;
+}
+
+QString MOBase::ModRepositoryFileInfo::toString() const
+{
+ return QString("[ "
+ "\"%1\",%2,\"%3\",\"%4\",\"%5\",\"%6\",%7,%8,%9,\"%10\",\"%11\",\"%"
+ "12\",%13,\"%14\",%15 ]")
+ .arg(gameName)
+ .arg(fileID)
+ .arg(name)
+ .arg(uri)
+ .arg(version.canonicalString())
+ .arg(description.mid(0).replace("\"", "'"))
+ .arg(categoryID)
+ .arg(fileSize)
+ .arg(modID)
+ .arg(modName)
+ .arg(newestVersion.canonicalString())
+ .arg(fileName)
+ .arg(fileCategory)
+ .arg(repository)
+ .arg(QString(QtJson::serialize(userData)))
+ .arg(author)
+ .arg(uploader)
+ .arg(uploaderUrl);
+}
diff --git a/libs/uibase/src/nxmurl.cpp b/libs/uibase/src/nxmurl.cpp
new file mode 100644
index 0000000..d09cdde
--- /dev/null
+++ b/libs/uibase/src/nxmurl.cpp
@@ -0,0 +1,44 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/nxmurl.h>
+#include <uibase/utility.h>
+#include <QList>
+#include <QRegularExpression>
+#include <QString>
+#include <QUrl>
+#include <QUrlQuery>
+
+NXMUrl::NXMUrl(const QString& url)
+{
+ QUrl nxm(url);
+ QUrlQuery query(nxm);
+ QRegularExpression exp("nxm://[a-z0-9]+/mods/(\\d+)/files/(\\d+)",
+ QRegularExpression::CaseInsensitiveOption);
+ auto match = exp.match(url);
+ if (!match.hasMatch()) {
+ throw MOBase::InvalidNXMLinkException(url);
+ }
+ m_Game = nxm.host();
+ m_ModId = match.captured(1).toInt();
+ m_FileId = match.captured(2).toInt();
+ m_Key = query.queryItemValue("key");
+ m_Expires = query.queryItemValue("expires").toInt();
+ m_UserId = query.queryItemValue("user_id").toInt();
+}
diff --git a/libs/uibase/src/pch.cpp b/libs/uibase/src/pch.cpp
new file mode 100644
index 0000000..1d9f38c
--- /dev/null
+++ b/libs/uibase/src/pch.cpp
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/libs/uibase/src/pch.h b/libs/uibase/src/pch.h
new file mode 100644
index 0000000..bd7e3bc
--- /dev/null
+++ b/libs/uibase/src/pch.h
@@ -0,0 +1,139 @@
+#ifdef _MSC_VER
+#pragma warning(disable : 4251) // neds to have dll-interface
+#pragma warning(disable : 4355) // this used in initializer list
+#pragma warning(disable : 4371) // layout may have changed
+#pragma warning(disable : 4514) // unreferenced inline function removed
+#pragma warning(disable : 4571) // catch semantics changed
+#pragma warning(disable : 4619) // no warning X
+#pragma warning(disable : 4623) // default constructor deleted
+#pragma warning(disable : 4625) // copy constructor deleted
+#pragma warning(disable : 4626) // copy assignment operator deleted
+#pragma warning(disable : 4710) // function not inlined
+#pragma warning(disable : 4820) // padding
+#pragma warning(disable : 4866) // left-to-right evaluation order
+#pragma warning(disable : 4868) // left-to-right evaluation order
+#pragma warning(disable : 5026) // move constructor deleted
+#pragma warning(disable : 5027) // move assignment operator deleted
+#pragma warning(disable : 5045) // spectre mitigation
+
+#pragma warning(push, 3)
+#pragma warning(disable : 4242) // uint -> const quint8
+#pragma warning(disable : 4365) // signed/unsigned mismatch
+#pragma warning(disable : 4668) // preprocessor macro used but not defined
+#pragma warning(disable : 4774) // bad format string
+#pragma warning(disable : 4946) // reinterpret_cast used between related classes
+#pragma warning(disable : 4800) // implicit conversion
+#pragma warning(disable : 5219) // implicit int -> float conversion
+#pragma warning( \
+ disable : 5249) // named enumerators with values outside of bit field width
+#endif // _MSC_VER
+
+#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING 1
+
+// std
+#include <algorithm>
+#include <cstdint>
+#include <cwctype>
+#include <filesystem>
+#include <functional>
+#include <iostream>
+#include <list>
+#include <map>
+#include <memory>
+#include <set>
+#include <sstream>
+#include <stdlib.h>
+#include <string>
+#include <typeindex>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+#include <wchar.h>
+
+// windows
+#ifdef _WIN32
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+#ifndef WIN32_MEAN_AND_LEAN
+#define WIN32_MEAN_AND_LEAN
+#endif
+#include <ShlObj.h>
+#include <Windows.h>
+#include <shobjidl.h>
+#endif // _WIN32
+
+// Qt
+#include <QAbstractButton>
+#include <QAction>
+#include <QApplication>
+#include <QBitmap>
+#include <QBuffer>
+#include <QCommandLinkButton>
+#include <QCoreApplication>
+#include <QCryptographicHash>
+#include <QDateTime>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QDir>
+#include <QDropEvent>
+#include <QFile>
+#include <QFileInfo>
+#include <QFlags>
+#include <QGraphicsObject>
+#include <QIcon>
+#include <QImage>
+#include <QLabel>
+#include <QLineEdit>
+#include <QList>
+#include <QMainWindow>
+#include <QMenuBar>
+#include <QMessageBox>
+#include <QMetaEnum>
+#include <QMetaMethod>
+#include <QMetaObject>
+#include <QMetaType>
+#include <QMouseEvent>
+#include <QMutex>
+#include <QMutexLocker>
+#include <QNetworkReply>
+#include <QObject>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QQuickItem>
+#include <QQuickWidget>
+#include <QRect>
+#include <QRegularExpression>
+#include <QResizeEvent>
+#include <QScreen>
+#include <QSettings>
+#include <QShortcutEvent>
+#include <QShowEvent>
+#include <QString>
+#include <QStyle>
+#include <QTabWidget>
+#include <QTableWidget>
+#include <QTemporaryFile>
+#include <QTextEdit>
+#include <QTextStream>
+#include <QTime>
+#include <QTimer>
+#include <QToolBar>
+#include <QToolButton>
+#include <QTreeWidget>
+#include <QUrl>
+#include <QUrlQuery>
+#include <QVBoxLayout>
+#include <QVariant>
+#include <QVariantMap>
+#include <QVersionNumber>
+#include <QWidget>
+#include <QtDebug>
+#include <qmetaobject.h>
+
+#undef _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
diff --git a/libs/uibase/src/pluginrequirements.cpp b/libs/uibase/src/pluginrequirements.cpp
new file mode 100644
index 0000000..a2a11ef
--- /dev/null
+++ b/libs/uibase/src/pluginrequirements.cpp
@@ -0,0 +1,138 @@
+#include <uibase/pluginrequirements.h>
+
+#include <QCoreApplication>
+
+#include <uibase/imoinfo.h>
+#include <uibase/iplugindiagnose.h>
+#include <uibase/iplugingame.h>
+
+using namespace MOBase;
+
+// Plugin and Game dependencies
+
+PluginDependencyRequirement::PluginDependencyRequirement(QStringList const& pluginNames)
+ : m_PluginNames(pluginNames)
+{}
+
+std::optional<IPluginRequirement::Problem>
+PluginDependencyRequirement::check(IOrganizer* o) const
+{
+ for (auto const& pluginName : m_PluginNames) {
+ if (o->isPluginEnabled(pluginName)) {
+ return {};
+ }
+ }
+ return Problem(message());
+}
+
+QString PluginDependencyRequirement::message() const
+{
+ if (m_PluginNames.size() > 1) {
+ return QObject::tr("One of the following plugins must be enabled: %1.")
+ .arg(m_PluginNames.join(", "));
+ } else {
+ return QObject::tr("This plugin can only be enabled if the '%1' plugin is "
+ "installed and enabled.")
+ .arg(m_PluginNames[0]);
+ }
+}
+
+GameDependencyRequirement::GameDependencyRequirement(QStringList const& gameNames)
+ : m_GameNames(gameNames)
+{}
+
+std::optional<IPluginRequirement::Problem>
+GameDependencyRequirement::check(IOrganizer* o) const
+{
+ auto* game = o->managedGame();
+ if (!game) {
+ return Problem(message());
+ }
+
+ QString gameName = game->gameName();
+ for (auto const& pluginName : m_GameNames) {
+ if (pluginName.compare(gameName, Qt::CaseInsensitive) == 0) {
+ return {};
+ }
+ }
+ return Problem(message());
+}
+
+QString GameDependencyRequirement::message() const
+{
+ return QObject::tr("This plugin can only be enabled for the following game(s): %1.",
+ "", static_cast<int>(m_GameNames.size()))
+ .arg(m_GameNames.join(", "));
+}
+
+// Diagnose requirements
+
+DiagnoseRequirement::DiagnoseRequirement(const IPluginDiagnose* diagnose)
+ : m_Diagnose(diagnose)
+{}
+
+std::optional<IPluginRequirement::Problem> DiagnoseRequirement::check(IOrganizer*) const
+{
+ auto activeProblems = m_Diagnose->activeProblems();
+
+ if (activeProblems.empty()) {
+ return {};
+ }
+
+ QStringList shortDescriptions, longDescriptions;
+ for (auto i : activeProblems) {
+ shortDescriptions.append(m_Diagnose->shortDescription(i));
+ longDescriptions.append(m_Diagnose->fullDescription(i));
+ }
+
+ return Problem(shortDescriptions.join("\n"), longDescriptions.join("\n"));
+}
+
+// Basic requirements
+class BasicPluginRequirement : public IPluginRequirement
+{
+public:
+ BasicPluginRequirement(std::function<bool(IOrganizer*)> const& checker,
+ QString const description)
+ : m_Checker(checker), m_Description(description)
+ {}
+
+ std::optional<Problem> check(IOrganizer* o) const
+ {
+ if (m_Checker(o)) {
+ return {};
+ }
+ return Problem(m_Description);
+ }
+
+private:
+ std::function<bool(IOrganizer*)> m_Checker;
+ QString m_Description;
+};
+
+// Factory
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::pluginDependency(QStringList const& pluginNames)
+{
+ return std::make_shared<PluginDependencyRequirement>(pluginNames);
+}
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::gameDependency(QStringList const& pluginGameNames)
+{
+ return std::make_shared<GameDependencyRequirement>(pluginGameNames);
+}
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::diagnose(const IPluginDiagnose* diagnose)
+{
+ return std::make_shared<DiagnoseRequirement>(diagnose);
+}
+
+std::shared_ptr<const IPluginRequirement>
+PluginRequirementFactory::basic(std::function<bool(IOrganizer*)> const& checker,
+ QString const description)
+{
+ return std::make_shared<BasicPluginRequirement>(checker, description);
+}
diff --git a/libs/uibase/src/questionboxmemory.cpp b/libs/uibase/src/questionboxmemory.cpp
new file mode 100644
index 0000000..586e52e
--- /dev/null
+++ b/libs/uibase/src/questionboxmemory.cpp
@@ -0,0 +1,200 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/questionboxmemory.h>
+#include <uibase/log.h>
+#include "ui_questionboxmemory.h"
+
+#include <QApplication>
+#include <QIcon>
+#include <QMutex>
+#include <QMutexLocker>
+#include <QPushButton>
+#include <QSettings>
+#include <QStyle>
+
+namespace MOBase
+{
+
+static QMutex g_mutex;
+static QuestionBoxMemory::GetButton g_get;
+static QuestionBoxMemory::SetWindowButton g_setWindow;
+static QuestionBoxMemory::SetFileButton g_setFile;
+
+QuestionBoxMemory::QuestionBoxMemory(QWidget* parent, const QString& title,
+ const QString& text, QString const* filename,
+ const QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+ : QDialog(parent), ui(new Ui::QuestionBoxMemory), m_Button(QDialogButtonBox::Cancel)
+{
+ ui->setupUi(this);
+
+ setWindowFlag(Qt::WindowType::WindowContextHelpButtonHint, false);
+ setWindowTitle(title);
+
+ QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion);
+ ui->iconLabel->setPixmap(icon.pixmap(128));
+ ui->messageLabel->setText(text);
+
+ if (filename == nullptr) {
+ // delete the 2nd check box
+ QCheckBox* box = ui->rememberForCheckBox;
+ box->parentWidget()->layout()->removeWidget(box);
+ delete box;
+ } else {
+ ui->rememberForCheckBox->setText(ui->rememberForCheckBox->text().arg(*filename));
+ }
+
+ ui->buttonBox->setStandardButtons(buttons);
+
+ if (defaultButton != QDialogButtonBox::NoButton) {
+ ui->buttonBox->button(defaultButton)->setDefault(true);
+ }
+
+ connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this,
+ SLOT(buttonClicked(QAbstractButton*)));
+}
+
+QuestionBoxMemory::~QuestionBoxMemory() = default;
+
+void QuestionBoxMemory::setCallbacks(GetButton get, SetWindowButton setWindow,
+ SetFileButton setFile)
+{
+ QMutexLocker locker(&g_mutex);
+
+ g_get = get;
+ g_setWindow = setWindow;
+ g_setFile = setFile;
+}
+
+void QuestionBoxMemory::buttonClicked(QAbstractButton* button)
+{
+ m_Button = ui->buttonBox->standardButton(button);
+}
+
+QDialogButtonBox::StandardButton
+QuestionBoxMemory::query(QWidget* parent, const QString& windowName,
+ const QString& title, const QString& text,
+ QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+{
+ return queryImpl(parent, windowName, nullptr, title, text, buttons, defaultButton);
+}
+
+QDialogButtonBox::StandardButton
+QuestionBoxMemory::query(QWidget* parent, const QString& windowName,
+ const QString& fileName, const QString& title,
+ const QString& text, QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+{
+ return queryImpl(parent, windowName, &fileName, title, text, buttons, defaultButton);
+}
+
+QDialogButtonBox::StandardButton
+QuestionBoxMemory::queryImpl(QWidget* parent, const QString& windowName,
+ const QString* fileName, const QString& title,
+ const QString& text,
+ QDialogButtonBox::StandardButtons buttons,
+ QDialogButtonBox::StandardButton defaultButton)
+{
+ QMutexLocker locker(&g_mutex);
+
+ const auto button = getMemory(windowName, (fileName ? *fileName : ""));
+ if (button != NoButton) {
+ log::debug("{}: not asking because user always wants response {}",
+ windowName + (fileName ? QString("/") + *fileName : ""),
+ buttonToString(button));
+
+ return button;
+ }
+
+ QuestionBoxMemory dialog(parent, title, text, fileName, buttons, defaultButton);
+ dialog.exec();
+
+ if (dialog.m_Button != QDialogButtonBox::Cancel) {
+ if (dialog.ui->rememberCheckBox->isChecked()) {
+ setWindowMemory(windowName, dialog.m_Button);
+ }
+
+ if (fileName != nullptr && dialog.ui->rememberForCheckBox->isChecked()) {
+ setFileMemory(windowName, *fileName, dialog.m_Button);
+ }
+ }
+
+ return dialog.m_Button;
+}
+
+void QuestionBoxMemory::setWindowMemory(const QString& windowName, Button b)
+{
+ log::debug("remembering choice {} for window {}", buttonToString(b), windowName);
+
+ g_setWindow(windowName, b);
+}
+
+void QuestionBoxMemory::setFileMemory(const QString& windowName,
+ const QString& filename, Button b)
+{
+ log::debug("remembering choice {} for file {}", buttonToString(b),
+ windowName + "/" + filename);
+
+ g_setFile(windowName, filename, b);
+}
+
+QuestionBoxMemory::Button QuestionBoxMemory::getMemory(const QString& windowName,
+ const QString& filename)
+{
+ return g_get(windowName, filename);
+}
+
+QString QuestionBoxMemory::buttonToString(Button b)
+{
+ using BB = QDialogButtonBox;
+
+ static const std::map<Button, QString> map = {
+ {BB::NoButton, "none"},
+ {BB::Ok, "ok"},
+ {BB::Save, "save"},
+ {BB::SaveAll, "saveall"},
+ {BB::Open, "open"},
+ {BB::Yes, "yes"},
+ {BB::YesToAll, "yestoall"},
+ {BB::No, "no"},
+ {BB::NoToAll, "notoall"},
+ {BB::Abort, "abort"},
+ {BB::Retry, "retry"},
+ {BB::Ignore, "ignore"},
+ {BB::Close, "close"},
+ {BB::Cancel, "cancel"},
+ {BB::Discard, "discard"},
+ {BB::Help, "help"},
+ {BB::Apply, "apply"},
+ {BB::Reset, "reset"},
+ {BB::RestoreDefaults, "restoredefaults"}};
+
+ auto itor = map.find(b);
+
+ if (itor == map.end()) {
+ return QString("0x%1").arg(static_cast<int>(b), 0, 16);
+ } else {
+ return QString("'%1' (0x%2)").arg(itor->second).arg(static_cast<int>(b), 0, 16);
+ }
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/questionboxmemory.ui b/libs/uibase/src/questionboxmemory.ui
new file mode 100644
index 0000000..8237571
--- /dev/null
+++ b/libs/uibase/src/questionboxmemory.ui
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QuestionBoxMemory</class>
+ <widget class="QDialog" name="QuestionBoxMemory">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>374</width>
+ <height>130</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string notr="true">Placeholder</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0">
+ <property name="spacing">
+ <number>1</number>
+ </property>
+ <property name="leftMargin">
+ <number>2</number>
+ </property>
+ <property name="topMargin">
+ <number>2</number>
+ </property>
+ <property name="rightMargin">
+ <number>2</number>
+ </property>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <widget class="QFrame" name="frame">
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0">
+ <item>
+ <widget class="QLabel" name="iconLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>36</width>
+ <height>36</height>
+ </size>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="messageLabel">
+ <property name="font">
+ <font>
+ <pointsize>9</pointsize>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="leftMargin">
+ <number>7</number>
+ </property>
+ <property name="topMargin">
+ <number>7</number>
+ </property>
+ <property name="rightMargin">
+ <number>7</number>
+ </property>
+ <property name="bottomMargin">
+ <number>7</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QCheckBox" name="rememberCheckBox">
+ <property name="text">
+ <string>Remember selection</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="rememberForCheckBox">
+ <property name="text">
+ <string>Remember selection only for %1</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::No|QDialogButtonBox::Yes</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>QuestionBoxMemory</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>QuestionBoxMemory</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/libs/uibase/src/registry.cpp b/libs/uibase/src/registry.cpp
new file mode 100644
index 0000000..0b6834f
--- /dev/null
+++ b/libs/uibase/src/registry.cpp
@@ -0,0 +1,189 @@
+/*
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/registry.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <QApplication>
+#include <QFile>
+#include <QFileInfo>
+#include <QList>
+#include <QMessageBox>
+#include <QString>
+#include <QTextStream>
+
+namespace MOBase
+{
+
+// Line-by-line INI writer that preserves the file format.
+// Unlike QSettings::IniFormat, this does NOT interpret backslashes as
+// line continuations, does NOT URL-encode spaces in key names, and does
+// NOT reorder keys. It only modifies the target key=value pair and
+// leaves everything else untouched.
+static bool writeIniValueDirect(const QString& section, const QString& key,
+ const QString& value, const QString& fileName)
+{
+ QStringList lines;
+ bool fileExists = QFileInfo::exists(fileName);
+
+ if (fileExists) {
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return false;
+ }
+ QTextStream in(&file);
+ while (!in.atEnd()) {
+ lines.append(in.readLine());
+ }
+ file.close();
+ }
+
+ // Find the target section and key
+ QString sectionHeader = "[" + section + "]";
+ int sectionStart = -1;
+ int sectionEnd = lines.size(); // end of file if section is last
+ int keyLine = -1;
+
+ for (int i = 0; i < lines.size(); ++i) {
+ QString trimmed = lines[i].trimmed();
+ if (trimmed.compare(sectionHeader, Qt::CaseInsensitive) == 0) {
+ sectionStart = i;
+ // Find end of this section (next section header or EOF)
+ for (int j = i + 1; j < lines.size(); ++j) {
+ QString t = lines[j].trimmed();
+ if (t.startsWith('[') && t.endsWith(']')) {
+ sectionEnd = j;
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ if (sectionStart >= 0) {
+ // Section found, look for the key within it
+ for (int i = sectionStart + 1; i < sectionEnd; ++i) {
+ QString trimmed = lines[i].trimmed();
+ // Skip comments and empty lines
+ if (trimmed.isEmpty() || trimmed.startsWith(';') || trimmed.startsWith('#')) {
+ continue;
+ }
+ int eqPos = trimmed.indexOf('=');
+ if (eqPos > 0) {
+ QString existingKey = trimmed.left(eqPos).trimmed();
+ if (existingKey.compare(key, Qt::CaseInsensitive) == 0) {
+ keyLine = i;
+ break;
+ }
+ }
+ }
+
+ if (keyLine >= 0) {
+ // Key found, replace the line preserving indentation
+ QString original = lines[keyLine];
+ int eqPos = original.indexOf('=');
+ // Preserve everything up to and including '='
+ lines[keyLine] = original.left(eqPos + 1) + value;
+ } else {
+ // Key not found in section, insert it after the section header
+ lines.insert(sectionStart + 1, key + "=" + value);
+ }
+ } else {
+ // Section not found, append it
+ if (!lines.isEmpty() && !lines.last().trimmed().isEmpty()) {
+ lines.append(""); // blank line before new section
+ }
+ lines.append(sectionHeader);
+ lines.append(key + "=" + value);
+ }
+
+ // Write back
+ QFile outFile(fileName);
+ if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ return false;
+ }
+ QTextStream out(&outFile);
+ for (int i = 0; i < lines.size(); ++i) {
+ out << lines[i];
+ if (i < lines.size() - 1) {
+ out << '\n';
+ }
+ }
+ // Preserve trailing newline if original had one, or add one
+ out << '\n';
+ outFile.close();
+
+ return true;
+}
+
+bool WriteRegistryValue(const QString& appName, const QString& keyName,
+ const QString& value, const QString& fileName)
+{
+ if (writeIniValueDirect(appName, keyName, value, fileName)) {
+ return true;
+ }
+
+ // Write failed, check if the file is read-only
+ QFileInfo fileInfo(fileName);
+
+ QMessageBox::StandardButton result =
+ MOBase::TaskDialog(qApp->activeModalWidget(),
+ QObject::tr("INI file is read-only"))
+ .main(QObject::tr("INI file is read-only"))
+ .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" "
+ "which is currently set to read-only.")
+ .arg(fileInfo.fileName()))
+ .icon(QMessageBox::Warning)
+ .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes})
+ .button({QObject::tr("Allow the write once"),
+ QObject::tr("The file will be set to read-only again."),
+ QMessageBox::Ignore})
+ .button({QObject::tr("Skip this file"), QMessageBox::No})
+ .remember("clearReadOnly", fileInfo.fileName())
+ .exec();
+
+ if (result & (QMessageBox::Yes | QMessageBox::Ignore)) {
+ // Make the file writable
+ QFile file(fileName);
+ file.setPermissions(file.permissions() | QFile::WriteUser | QFile::WriteOwner);
+
+ bool ok = writeIniValueDirect(appName, keyName, value, fileName);
+
+ if (result == QMessageBox::Ignore) {
+ // Set back to read-only
+ file.setPermissions(file.permissions() & ~(QFile::WriteUser | QFile::WriteOwner));
+ }
+
+ return ok;
+ }
+
+ return false;
+}
+
+#ifdef _WIN32
+bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName,
+ const wchar_t* value, const wchar_t* fileName)
+{
+ return WriteRegistryValue(
+ QString::fromWCharArray(appName),
+ QString::fromWCharArray(keyName),
+ QString::fromWCharArray(value),
+ QString::fromWCharArray(fileName));
+}
+#endif
+
+} // namespace MOBase
diff --git a/libs/uibase/src/report.cpp b/libs/uibase/src/report.cpp
new file mode 100644
index 0000000..1c47d95
--- /dev/null
+++ b/libs/uibase/src/report.cpp
@@ -0,0 +1,453 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/report.h>
+#include <uibase/expanderwidget.h>
+#include <uibase/log.h>
+#include <uibase/questionboxmemory.h>
+#include "ui_taskdialog.h"
+#include <uibase/utility.h>
+#include <QComboBox>
+#include <QMainWindow>
+#include <QRadioButton>
+#include <QThread>
+
+namespace MOBase
+{
+
+QWidget* topLevelWindow()
+{
+ if (!qApp) {
+ return nullptr;
+ }
+
+ for (QWidget* w : qApp->topLevelWidgets()) {
+ if (dynamic_cast<QMainWindow*>(w)) {
+ return w;
+ }
+ }
+
+ return nullptr;
+}
+
+void criticalOnTop(const QString& message)
+{
+ QMessageBox mb(QMessageBox::Critical, "Mod Organizer", message);
+
+ mb.show();
+ mb.activateWindow();
+ mb.raise();
+ mb.exec();
+}
+
+void reportError(const QString& message)
+{
+ log::error("{}", message);
+
+ // QMessageBox must only be created on the main (GUI) thread.
+ // If we're on a background thread, just log — showing a dialog would crash.
+ if (QThread::currentThread() != qApp->thread()) {
+ return;
+ }
+
+ if (QApplication::topLevelWidgets().count() != 0) {
+ if (auto* mw = topLevelWindow()) {
+ QMessageBox::warning(mw, QObject::tr("Error"), message, QMessageBox::Ok);
+ } else {
+ criticalOnTop(message);
+ }
+ } else {
+ // No top-level widget available, show a standalone critical dialog
+ criticalOnTop(message);
+ }
+}
+
+TaskDialogButton::TaskDialogButton(QString t, QString d, QMessageBox::StandardButton b)
+ : text(std::move(t)), description(std::move(d)), button(b)
+{}
+
+TaskDialogButton::TaskDialogButton(QString t, QMessageBox::StandardButton b)
+ : TaskDialogButton(std::move(t), {}, b)
+{}
+
+TaskDialog::TaskDialog(QWidget* parent, QString title)
+ : m_dialog(new QDialog(parent)), ui(new Ui::TaskDialog), m_title(std::move(title)),
+ m_icon(QMessageBox::NoIcon), m_result(QMessageBox::Cancel), m_width(-1),
+ m_rememberCheck(nullptr), m_rememberCombo(nullptr)
+{
+ ui->setupUi(m_dialog.get());
+}
+
+TaskDialog::~TaskDialog() = default;
+
+TaskDialog& TaskDialog::title(const QString& s)
+{
+ m_title = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::main(const QString& s)
+{
+ m_main = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::content(const QString& s)
+{
+ m_content = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::details(const QString& s)
+{
+ m_details = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::icon(QMessageBox::Icon i)
+{
+ m_icon = i;
+ return *this;
+}
+
+TaskDialog& TaskDialog::button(TaskDialogButton b)
+{
+ m_buttons.emplace_back(std::move(b));
+ return *this;
+}
+
+TaskDialog& TaskDialog::remember(const QString& action, const QString& file)
+{
+ m_rememberAction = action;
+ m_rememberFile = file;
+ return *this;
+}
+
+void TaskDialog::addContent(QWidget* w)
+{
+ auto* ly = static_cast<QVBoxLayout*>(ui->contentPanel->layout());
+
+ // add before spacer
+ ly->insertWidget(ly->count() - 1, w);
+}
+
+void TaskDialog::setWidth(int w)
+{
+ m_width = w;
+}
+
+QMessageBox::StandardButton TaskDialog::exec()
+{
+ const auto b = checkMemory();
+ if (b != QMessageBox::NoButton) {
+ return b;
+ }
+
+ setDialog();
+ setWidgets();
+ setChoices();
+ setButtons();
+ setDetails();
+
+ if (m_width >= 0) {
+ ui->topPanel->setMinimumWidth(m_width);
+ } else {
+ ui->topPanel->setMinimumWidth(400);
+ }
+
+ m_dialog->adjustSize();
+
+ if (!m_dialog->parent()) {
+ // no parent, make sure it's shown on top
+ m_dialog->show();
+ m_dialog->activateWindow();
+ m_dialog->raise();
+ }
+
+ if (m_dialog->exec() != QDialog::Accepted) {
+ return QMessageBox::Cancel;
+ }
+
+ rememberChoice();
+ return m_result;
+}
+
+QMessageBox::StandardButton TaskDialog::checkMemory() const
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ return QMessageBox::NoButton;
+ }
+
+ const auto b = QuestionBoxMemory::getMemory(m_rememberAction, m_rememberFile);
+
+ const auto logName = m_rememberAction +
+ (m_rememberFile.isEmpty() ? "" : QString("/") + m_rememberFile);
+
+ if (b == QDialogButtonBox::NoButton) {
+ log::debug("{}: asking because the user has not set a choice before", logName);
+ } else {
+ log::debug("{}: not asking because user always wants response {}", logName,
+ QuestionBoxMemory::buttonToString(b));
+ }
+
+ return static_cast<QMessageBox::StandardButton>(b);
+}
+
+void TaskDialog::rememberChoice()
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ // nothing
+ return;
+ }
+
+ const auto b = static_cast<QuestionBoxMemory::Button>(m_result);
+
+ if (m_rememberCheck) {
+ // action only
+ if (m_rememberCheck->isChecked()) {
+ QuestionBoxMemory::setWindowMemory(m_rememberAction, b);
+ }
+ } else if (m_rememberCombo) {
+ Q_ASSERT(m_rememberCombo->count() == 3);
+
+ if (m_rememberCombo->currentIndex() == 1) {
+ // remember action
+ QuestionBoxMemory::setWindowMemory(m_rememberAction, b);
+ } else if (m_rememberCombo->currentIndex() == 2) {
+ // remember file
+ QuestionBoxMemory::setFileMemory(m_rememberAction, m_rememberFile, b);
+ }
+ }
+}
+
+void TaskDialog::setButtons()
+{
+ if (m_buttons.empty()) {
+ setStandardButtons();
+ } else {
+ setCommandButtons();
+ }
+
+ m_expander.reset(new ExpanderWidget(ui->detailsExpander, ui->detailsWidget));
+}
+
+void TaskDialog::setStandardButtons()
+{
+ ui->standardButtonsPanel->show();
+ ui->commandButtonsPanel->hide();
+
+ for (auto* b : ui->standardButtons->buttons()) {
+ ui->standardButtons->removeButton(b);
+ }
+
+ ui->standardButtons->addButton(QDialogButtonBox::Ok);
+
+ QObject::connect(ui->standardButtons, &QDialogButtonBox::clicked, [&](auto* b) {
+ m_result = static_cast<QMessageBox::StandardButton>(
+ ui->standardButtons->standardButton(b));
+
+ m_dialog->accept();
+ });
+}
+
+void TaskDialog::setCommandButtons()
+{
+ ui->standardButtonsPanel->hide();
+ ui->commandButtonsPanel->show();
+
+ deleteChildWidgets(ui->commandButtons);
+
+ for (auto&& b : m_buttons) {
+ auto* cb = new QCommandLinkButton(b.text, b.description);
+
+ QObject::connect(cb, &QAbstractButton::clicked, [&] {
+ m_result = b.button;
+ m_dialog->accept();
+ });
+
+ ui->commandButtons->layout()->addWidget(cb);
+ }
+}
+
+void TaskDialog::setDetails()
+{
+ if (m_details.isEmpty()) {
+ ui->detailsExpander->setEnabled(false);
+ return;
+ }
+
+ ui->details->setPlainText(m_details);
+
+ setVisibleLines(ui->details, 10);
+ setFontPercent(ui->details, 0.9);
+
+ const QColor bg = detailsColor();
+
+ ui->detailsPanel->setStyleSheet(QString("background-color: rgb(%1, %2, %3)")
+ .arg(bg.redF() * 255)
+ .arg(bg.greenF() * 255)
+ .arg(bg.blueF() * 255));
+}
+
+void TaskDialog::setDialog()
+{
+#ifdef _WIN32
+ m_dialog->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
+#else
+ m_dialog->setWindowFlags(Qt::Dialog);
+#endif
+ m_dialog->layout()->setSizeConstraint(QLayout::SetFixedSize);
+
+ m_dialog->setWindowTitle(m_title);
+}
+
+void TaskDialog::setWidgets()
+{
+ ui->main->setText(m_main);
+ setFontPercent(ui->main, 1.5);
+
+ ui->content->setText(m_content);
+ ui->content->setVisible(!m_content.isEmpty());
+
+ auto icon = standardIcon(m_icon);
+
+ if (icon.isNull()) {
+ ui->iconPanel->hide();
+ } else {
+ ui->iconPanel->show();
+ ui->icon->setPixmap(std::move(icon));
+ }
+}
+
+void TaskDialog::setChoices()
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ ui->rememberPanel->hide();
+ return;
+ }
+
+ ui->rememberPanel->show();
+ deleteChildWidgets(ui->rememberPanel);
+
+ const auto tooltip = QObject::tr(
+ "You can reset these choices by clicking \"Reset Dialog Choices\" in the "
+ "General tab of the Settings");
+
+ if (!m_rememberAction.isEmpty() && !m_rememberFile.isEmpty()) {
+ // both
+ m_rememberCombo = new QComboBox;
+ m_rememberCombo->setToolTip(tooltip);
+
+ m_rememberCombo->addItem(QObject::tr("Always ask"));
+ m_rememberCombo->addItem(QObject::tr("Remember my choice"));
+ m_rememberCombo->addItem(
+ QObject::tr("Remember my choice for %1").arg(m_rememberFile));
+
+ ui->rememberPanel->layout()->setAlignment(Qt::AlignLeft);
+ ui->rememberPanel->layout()->addWidget(m_rememberCombo);
+ } else if (!m_rememberAction.isEmpty() || !m_rememberFile.isEmpty()) {
+ // either
+ m_rememberCheck = new QCheckBox(QObject::tr("Remember my choice"));
+ m_rememberCheck->setToolTip(tooltip);
+ ui->rememberPanel->layout()->addWidget(m_rememberCheck);
+ }
+}
+
+QColor TaskDialog::detailsColor() const
+{
+ auto b = std::make_unique<QPushButton>();
+ m_dialog->style()->polish(b.get());
+ return b->palette().color(b->backgroundRole());
+}
+
+QPixmap TaskDialog::standardIcon(QMessageBox::Icon icon) const
+{
+ QStyle* s = m_dialog->style();
+ QIcon i;
+
+ switch (icon) {
+ case QMessageBox::Information:
+ i = s->standardIcon(QStyle::SP_MessageBoxInformation, 0, m_dialog.get());
+ break;
+ case QMessageBox::Warning:
+ i = s->standardIcon(QStyle::SP_MessageBoxWarning, 0, m_dialog.get());
+ break;
+ case QMessageBox::Critical:
+ i = s->standardIcon(QStyle::SP_MessageBoxCritical, 0, m_dialog.get());
+ break;
+ case QMessageBox::Question:
+ i = s->standardIcon(QStyle::SP_MessageBoxQuestion, 0, m_dialog.get());
+ break;
+ case QMessageBox::NoIcon:
+ [[fallthrough]];
+ default:
+ break;
+ }
+
+ if (i.isNull()) {
+ return {};
+ }
+
+ QWindow* window = m_dialog->windowHandle();
+ if (!window) {
+ if (const QWidget* nativeParent = m_dialog->nativeParentWidget())
+ window = nativeParent->windowHandle();
+ }
+
+ const int iconSize = s->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, m_dialog.get());
+
+ return i.pixmap(QSize(iconSize, iconSize));
+}
+
+void TaskDialog::setVisibleLines(QPlainTextEdit* w, int lines)
+{
+ QTextDocument* d = w->document();
+ QFontMetrics fm(d->defaultFont());
+
+ const QMargins margins = ui->details->contentsMargins();
+
+ double height = 0;
+
+ // lines
+ height += fm.lineSpacing() * lines;
+
+ // top and bottom margins for document and frame
+ height += (d->documentMargin() + ui->details->frameWidth()) * 2;
+
+ // widget margins
+ height += margins.top() + margins.bottom();
+
+ w->setMaximumHeight(static_cast<int>(std::round(height)));
+}
+
+void TaskDialog::setFontPercent(QWidget* w, double p)
+{
+ auto f = w->font();
+
+ if (f.pointSizeF() > 0) {
+ f.setPointSizeF(f.pointSizeF() * p);
+ } else if (f.pixelSize() > 0) {
+ f.setPixelSize(static_cast<int>(std::round(f.pixelSize() * p)));
+ }
+
+ w->setFont(f);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/safewritefile.cpp b/libs/uibase/src/safewritefile.cpp
new file mode 100644
index 0000000..fb0819f
--- /dev/null
+++ b/libs/uibase/src/safewritefile.cpp
@@ -0,0 +1,84 @@
+/*
+Copyright (C) 2014 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/safewritefile.h>
+#include <uibase/log.h>
+#include <QStorageInfo>
+#include <QString>
+
+namespace MOBase
+{
+
+#ifdef _WIN32
+
+SafeWriteFile::SafeWriteFile(const QString& fileName) : m_SaveFile(fileName)
+{
+ if (!m_SaveFile.open(QIODeviceBase::WriteOnly)) {
+ const auto av =
+ static_cast<double>(QStorageInfo(m_SaveFile.fileName()).bytesAvailable());
+
+ log::error(
+ "failed to create temporary file for '{}', error {} ('{}'), {:.3f}GB available",
+ m_SaveFile.fileName(), m_SaveFile.error(), m_SaveFile.errorString(),
+ (av / 1024 / 1024 / 1024));
+
+ throw Exception(
+ QObject::tr(
+ "Failed to save '%1', could not create a temporary file: %2 (error %3)")
+ .arg(m_SaveFile.fileName())
+ .arg(m_SaveFile.errorString())
+ .arg(m_SaveFile.error()));
+ }
+}
+
+QSaveFile* SafeWriteFile::operator->()
+{
+ Q_ASSERT(m_SaveFile.isOpen());
+ return &m_SaveFile;
+}
+
+#else // Linux — use plain QFile (QSaveFile is unreliable across filesystems)
+
+SafeWriteFile::SafeWriteFile(const QString& fileName) : m_File(fileName)
+{
+ if (!m_File.open(QIODeviceBase::WriteOnly | QIODeviceBase::Truncate)) {
+ const auto av =
+ static_cast<double>(QStorageInfo(m_File.fileName()).bytesAvailable());
+
+ log::error("failed to open '{}' for writing, error {} ('{}'), {:.3f}GB available",
+ m_File.fileName(), m_File.error(), m_File.errorString(),
+ (av / 1024 / 1024 / 1024));
+
+ throw Exception(
+ QObject::tr("Failed to save '%1': %2 (error %3)")
+ .arg(m_File.fileName())
+ .arg(m_File.errorString())
+ .arg(m_File.error()));
+ }
+}
+
+DirectWriteFile* SafeWriteFile::operator->()
+{
+ Q_ASSERT(m_File.isOpen());
+ return &m_File;
+}
+
+#endif
+
+} // namespace MOBase
diff --git a/libs/uibase/src/scopeguard.cpp b/libs/uibase/src/scopeguard.cpp
new file mode 100644
index 0000000..a671d1e
--- /dev/null
+++ b/libs/uibase/src/scopeguard.cpp
@@ -0,0 +1,6 @@
+#include <uibase/scopeguard.h>
+
+namespace MOBase
+{
+
+} // namespace MOBase
diff --git a/libs/uibase/src/sortabletreewidget.cpp b/libs/uibase/src/sortabletreewidget.cpp
new file mode 100644
index 0000000..5cfbdd4
--- /dev/null
+++ b/libs/uibase/src/sortabletreewidget.cpp
@@ -0,0 +1,116 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2013 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/sortabletreewidget.h>
+#include <QDropEvent>
+
+namespace MOBase
+{
+
+SortableTreeWidget::SortableTreeWidget(QWidget* parent)
+ : QTreeWidget(parent), m_LocalMoveOnly(false)
+{}
+
+void SortableTreeWidget::setLocalMoveOnly(bool localOnly)
+{
+ m_LocalMoveOnly = localOnly;
+}
+
+void SortableTreeWidget::dropEvent(QDropEvent* event)
+{
+ // only react on internal drag&drop
+ if (event->source() == this) {
+ QTreeWidget::dropEvent(event);
+ }
+}
+
+Qt::DropActions SortableTreeWidget::supportedDropActions() const
+{
+ // we do our own drop handling, this ensures dropMimeData is called.
+ return Qt::CopyAction;
+}
+
+bool SortableTreeWidget::dropMimeData(QTreeWidgetItem* parent, int index,
+ const QMimeData*, Qt::DropAction)
+{
+ // TODO: ignores type of action set up in designer, always moves
+ return this->moveSelection(parent, index);
+}
+
+bool SortableTreeWidget::moveSelection(QTreeWidgetItem* parent, int idx)
+{
+ QModelIndex parentIndex = indexFromItem(parent);
+ std::vector<QPersistentModelIndex> persistentIndices;
+ Q_FOREACH (const QModelIndex& index, selectedIndexes()) {
+ if (index == parentIndex)
+ return false;
+ if (m_LocalMoveOnly && (parentIndex != index.parent()))
+ return false;
+ if (index.column() != 0)
+ continue;
+ persistentIndices.push_back(index);
+ }
+
+ int targetRow = -1;
+ if (itemsExpandable() || !parentIndex.isValid()) {
+ targetRow = model()->index(idx, 0, parentIndex).row();
+
+ if (targetRow < 0) {
+ targetRow = idx;
+ }
+ } else {
+ // if items aren't expandable, we can't be placing items on sublevels
+ targetRow = parentIndex.row();
+ parentIndex = QModelIndex();
+ }
+
+ // remove items from the list, store in temporary location
+ QList<QTreeWidgetItem*> temp;
+ for (auto iter = persistentIndices.rbegin(); iter != persistentIndices.rend();
+ ++iter) {
+ QTreeWidgetItem* item = itemFromIndex(*iter);
+ if (item == nullptr)
+ continue;
+ if ((item == nullptr) || (item->parent() == nullptr)) {
+ temp.append(takeTopLevelItem(iter->row()));
+ } else {
+ temp.append(item->parent()->takeChild(iter->row()));
+ }
+ }
+
+ if (idx == -1) {
+ // append
+ if (parentIndex.isValid()) {
+ parent->addChildren(temp);
+ } else {
+ addTopLevelItems(temp);
+ }
+ } else {
+ if (parentIndex.isValid()) {
+ parent->insertChildren(std::min<int>(targetRow, parent->childCount()), temp);
+ } else {
+ insertTopLevelItems(std::min<int>(targetRow, topLevelItemCount()), temp);
+ }
+ }
+ emit itemsMoved();
+ return true;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/steamutility.cpp b/libs/uibase/src/steamutility.cpp
new file mode 100644
index 0000000..a9dd80a
--- /dev/null
+++ b/libs/uibase/src/steamutility.cpp
@@ -0,0 +1,108 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2019 MO2 Contributors. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/steamutility.h>
+
+#include <QDir>
+#include <QList>
+#include <QRegularExpression>
+#include <QSettings>
+#include <QString>
+#include <QTextStream>
+
+namespace MOBase
+{
+
+// Lines that contains libraries are in the format:
+// "1" "Path\to\library"
+static const QRegularExpression
+ kSteamLibraryFilter("^\\s*\"(?<idx>[0-9]+)\"\\s*\"(?<path>.*)\"");
+
+QString findSteam()
+{
+#ifdef _WIN32
+ QSettings steamRegistry("Valve", "Steam");
+ return steamRegistry.value("SteamPath").toString();
+#else
+ // On Linux, check common Steam locations
+ QString homePath = QDir::homePath();
+ QStringList candidates = {
+ homePath + "/.steam/steam",
+ homePath + "/.local/share/Steam",
+ homePath + "/.steam/debian-installation"
+ };
+ for (const auto& path : candidates) {
+ if (QDir(path).exists()) {
+ return path;
+ }
+ }
+ return "";
+#endif
+}
+
+QString findSteamGame(const QString& appName, const QString& validFile)
+{
+ QStringList libraryFolders; // list of Steam libraries to search
+ QDir steamDir(findSteam()); // Steam installation directory
+
+ // Can do nothing if Steam doesn't exist
+ if (!steamDir.exists())
+ return "";
+
+ // The Steam install is always a valid library
+ libraryFolders << steamDir.absolutePath();
+
+ // Search libraryfolders.vdf for additional libraries
+ QFile libraryFoldersFile(steamDir.absoluteFilePath("steamapps/libraryfolders.vdf"));
+ if (libraryFoldersFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ QTextStream in(&libraryFoldersFile);
+ while (!in.atEnd()) {
+ QString line = in.readLine();
+ QRegularExpressionMatch match = kSteamLibraryFilter.match(line);
+ if (match.hasMatch()) {
+ QString folder = match.captured("path");
+#ifdef _WIN32
+ folder.replace("/", "\\").replace("\\\\", "\\");
+#else
+ folder.replace("\\\\", "/").replace("\\", "/");
+#endif
+ libraryFolders << folder;
+ }
+ }
+ }
+
+ // Search the Steam libraries for the game directory
+ for (auto library : libraryFolders) {
+ QDir libraryDir(library);
+ if (!libraryDir.cd("steamapps/common/" + appName))
+ continue;
+ QString normalizedValidFile = validFile;
+ normalizedValidFile.replace("\\", "/");
+ if (normalizedValidFile.startsWith('/')) {
+ normalizedValidFile.remove(0, 1);
+ }
+ if (normalizedValidFile.isEmpty() || libraryDir.exists(normalizedValidFile))
+ return libraryDir.absolutePath();
+ }
+
+ return "";
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/strings.cpp b/libs/uibase/src/strings.cpp
new file mode 100644
index 0000000..11087f4
--- /dev/null
+++ b/libs/uibase/src/strings.cpp
@@ -0,0 +1,46 @@
+#include <uibase/strings.h>
+
+#include <algorithm>
+#include <locale>
+
+namespace MOBase
+{
+
+// this is strongly inspired from boost
+class is_iequal
+{
+ std::locale m_loc;
+
+public:
+ is_iequal(const std::locale& loc = std::locale()) : m_loc{loc} {}
+
+ template <typename T1, typename T2>
+ bool operator()(const T1& Arg1, const T2& Arg2) const
+ {
+ return std::toupper<T1>(Arg1, m_loc) == std::toupper<T2>(Arg2, m_loc);
+ }
+};
+
+bool iequals(std::string_view lhs, std::string_view rhs)
+{
+ return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), is_iequal());
+}
+
+void ireplace_all(std::string& input, std::string_view search,
+ std::string_view replace) noexcept
+{
+ const auto search_length = static_cast<std::string::difference_type>(search.size());
+ const auto replace_length = replace.size();
+
+ std::size_t i = 0;
+ while (input.size() - i >= search_length) {
+ if (iequals(std::string_view(input).substr(i, search_length), search)) {
+ input.replace(i, search_length, replace);
+ i += replace_length;
+ } else {
+ ++i;
+ }
+ }
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/taskdialog.ui b/libs/uibase/src/taskdialog.ui
new file mode 100644
index 0000000..727312d
--- /dev/null
+++ b/libs/uibase/src/taskdialog.ui
@@ -0,0 +1,359 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TaskDialog</class>
+ <widget class="QDialog" name="TaskDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>436</width>
+ <height>316</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Dialog</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="topPanel" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
+ <item>
+ <widget class="QWidget" name="iconPanel" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="leftMargin">
+ <number>9</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>9</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="icon">
+ <property name="text">
+ <string>icon</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="mainPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="main">
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="text">
+ <string>dummy main text</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="mainSpacer" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>10</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="contentPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="content">
+ <property name="text">
+ <string>dummy content text</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="buttonsSpacer" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>10</height>
+ </size>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_7"/>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="commandButtonsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="commandButtons" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCommandLinkButton" name="dummyButton">
+ <property name="text">
+ <string>dummy button</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="rememberPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="dummyCheckbox">
+ <property name="text">
+ <string>dummy checkbox</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QWidget" name="standardButtonsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QDialogButtonBox" name="standardButtons">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Close</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="detailsPanel" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="widget_7" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QToolButton" name="detailsExpander">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">border: none;
+text-align: left;</string>
+ </property>
+ <property name="text">
+ <string>Details</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="detailsWidget" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>9</number>
+ </property>
+ <item>
+ <widget class="QPlainTextEdit" name="details">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/uibase/src/taskprogressmanager.cpp b/libs/uibase/src/taskprogressmanager.cpp
new file mode 100644
index 0000000..3bcffb0
--- /dev/null
+++ b/libs/uibase/src/taskprogressmanager.cpp
@@ -0,0 +1,125 @@
+#include <uibase/taskprogressmanager.h>
+#include <uibase/log.h>
+#include <uibase/utility.h>
+#include <QApplication>
+#include <QMainWindow>
+#include <QWidget>
+
+namespace MOBase
+{
+
+TaskProgressManager& TaskProgressManager::instance()
+{
+ static TaskProgressManager s_Instance;
+ return s_Instance;
+}
+
+void TaskProgressManager::forgetMe(quint32 id)
+{
+ if (m_Taskbar == nullptr) {
+ return;
+ }
+ auto iter = m_Percentages.find(id);
+ if (iter != m_Percentages.end()) {
+ m_Percentages.erase(iter);
+ }
+ showProgress();
+}
+
+void TaskProgressManager::updateProgress(quint32 id, qint64 value, qint64 max)
+{
+ QMutexLocker lock(&m_Mutex);
+ if (m_Taskbar == nullptr) {
+ return;
+ }
+
+ if (value == max) {
+ auto iter = m_Percentages.find(id);
+ if (iter != m_Percentages.end()) {
+ m_Percentages.erase(iter);
+ }
+ } else {
+ m_Percentages[id] = std::make_pair(QTime::currentTime(), (value * 100) / max);
+ }
+
+ showProgress();
+}
+
+quint32 TaskProgressManager::getId()
+{
+ QMutexLocker lock(&m_Mutex);
+ return m_NextId++;
+}
+
+void TaskProgressManager::showProgress()
+{
+#ifdef _WIN32
+ if (!m_Percentages.empty()) {
+ m_Taskbar->SetProgressState(m_WinId, TBPF_NORMAL);
+
+ QTime now = QTime::currentTime();
+ unsigned long long total = 0;
+ unsigned long long count = 0;
+
+ for (auto iter = m_Percentages.begin(); iter != m_Percentages.end();) {
+ if (iter->second.first.secsTo(now) < 15) {
+ total += static_cast<unsigned long long>(iter->second.second);
+ ++iter;
+ ++count;
+ } else {
+ log::debug("no progress in 15 seconds ({})", iter->second.first.secsTo(now));
+ iter = m_Percentages.erase(iter);
+ }
+ }
+
+ m_Taskbar->SetProgressValue(m_WinId, total, count * 100);
+ } else {
+ m_Taskbar->SetProgressState(m_WinId, TBPF_NOPROGRESS);
+ }
+#else
+ // On Linux, taskbar progress is not supported in this implementation.
+ // Could integrate with D-Bus com.canonical.Unity.LauncherEntry or similar.
+ (void)m_Percentages;
+#endif
+}
+
+bool TaskProgressManager::tryCreateTaskbar()
+{
+#ifdef _WIN32
+ // try to find our main window
+ for (QWidget* widget : QApplication::topLevelWidgets()) {
+ QMainWindow* mainWin = qobject_cast<QMainWindow*>(widget);
+ if (mainWin != nullptr) {
+ m_WinId = reinterpret_cast<HWND>(mainWin->winId());
+ }
+ }
+
+ HRESULT result = 0;
+ if (m_WinId != nullptr) {
+ result = CoCreateInstance(CLSID_TaskbarList, 0, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(&m_Taskbar));
+ if (result == S_OK) {
+ return true;
+ }
+ }
+
+ m_Taskbar = nullptr;
+
+ if (m_CreateTries-- > 0) {
+ QTimer::singleShot(1000, this, SLOT(tryCreateTaskbar()));
+ } else {
+ log::warn("failed to create taskbar connection");
+ }
+ return false;
+#else
+ // On Linux, no Windows taskbar API
+ m_Taskbar = nullptr;
+ return false;
+#endif
+}
+
+TaskProgressManager::TaskProgressManager()
+ : m_NextId(1), m_CreateTries(10), m_WinId(nullptr), m_Taskbar(nullptr)
+{}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/textviewer.cpp b/libs/uibase/src/textviewer.cpp
new file mode 100644
index 0000000..8c7ac0f
--- /dev/null
+++ b/libs/uibase/src/textviewer.cpp
@@ -0,0 +1,278 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/textviewer.h>
+#include <uibase/finddialog.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include "ui_textviewer.h"
+#include <uibase/utility.h>
+#include <QAction>
+#include <QFile>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QPushButton>
+#include <QShortcutEvent>
+#include <QTextEdit>
+#include <QVBoxLayout>
+
+namespace MOBase
+{
+
+TextViewer::TextViewer(const QString& title, QWidget* parent)
+ : QDialog(parent), ui(new Ui::TextViewer), m_FindDialog(nullptr)
+{
+ ui->setupUi(this);
+ setWindowTitle(title);
+ m_EditorTabs = findChild<QTabWidget*>("editorTabs");
+ connect(ui->showWhitespace, SIGNAL(stateChanged(int)), this,
+ SLOT(showWhitespaceChanged(int)));
+}
+
+TextViewer::~TextViewer()
+{
+ delete ui;
+}
+
+void TextViewer::closeEvent(QCloseEvent* event)
+{
+ if (!m_Modified.empty()) {
+ for (std::set<QTextEdit*>::iterator iter = m_Modified.begin();
+ iter != m_Modified.end(); ++iter) {
+ QMessageBox::StandardButton res = QMessageBox::question(
+ this, tr("Save changes?"),
+ tr("Do you want to save changes to %1?").arg((*iter)->documentTitle()),
+ QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
+ if (res == QMessageBox::Yes) {
+ saveFile(*iter);
+ } else if (res == QMessageBox::Cancel) {
+ event->ignore();
+ break;
+ }
+ }
+ }
+}
+
+void TextViewer::find()
+{
+ if (!m_FindDialog) {
+ m_FindDialog = new FindDialog(this);
+ connect(m_FindDialog, SIGNAL(findNext()), this, SLOT(findNext()));
+ connect(m_FindDialog, SIGNAL(patternChanged(QString)), this,
+ SLOT(patternChanged(QString)));
+ }
+
+ m_FindDialog->show();
+ m_FindDialog->raise();
+ m_FindDialog->activateWindow();
+}
+
+void TextViewer::patternChanged(QString newPattern)
+{
+ m_FindPattern = newPattern;
+}
+
+void TextViewer::findNext()
+{
+ if (m_FindPattern.length() == 0) {
+ return;
+ }
+
+ QWidget* currentPage = m_EditorTabs->currentWidget();
+ QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView");
+
+ if (editor->find(m_FindPattern)) {
+ // found text
+ return;
+ } else {
+ // reached the bottom and no text found,
+ // we wrap around once.
+
+ // save current cursor
+ auto oldCursor = editor->textCursor();
+
+ editor->moveCursor(QTextCursor::Start);
+
+ // search again from the top
+ if (editor->find(m_FindPattern)) {
+ // found something, keep new cursor position.
+ return;
+ } else {
+ // there are no matches in the document,
+ // restore previous cursor.
+ editor->setTextCursor(oldCursor);
+ }
+ }
+}
+
+void TextViewer::showWhitespaceChanged(int state)
+{
+ for (int i = 0; i < m_EditorTabs->count(); ++i) {
+ QTextEdit* editor = m_EditorTabs->widget(i)->findChild<QTextEdit*>();
+ if (editor != nullptr) {
+ auto document = editor->document();
+ auto textOption = document->defaultTextOption();
+ auto flags = textOption.flags();
+ if (state == Qt::Unchecked)
+ flags = flags & (~QTextOption::ShowTabsAndSpaces);
+ else
+ flags = flags | QTextOption::ShowTabsAndSpaces;
+ textOption.setFlags(flags);
+ document->setDefaultTextOption(textOption);
+ editor->setDocument(document);
+ }
+ }
+}
+
+bool TextViewer::eventFilter(QObject* object, QEvent* event)
+{
+ if (event->type() == QEvent::ShortcutOverride) {
+ QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
+ if (keyEvent->matches(QKeySequence::Find)) {
+ find();
+ } else if (keyEvent->matches(QKeySequence::FindNext)) {
+ findNext();
+ }
+ }
+ return QDialog::eventFilter(object, event);
+}
+
+void TextViewer::setDescription(const QString& description)
+{
+ QLabel* descriptionLabel = findChild<QLabel*>("descriptionLabel");
+ descriptionLabel->setText(description);
+}
+
+void TextViewer::saveFile(const QTextEdit* editor)
+{
+ bool write = true;
+ QMessageBox::StandardButton buttonPressed = QMessageBox::Ignore;
+ QFile file(editor->documentTitle());
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ write = false;
+ QFileInfo fileInfo(file.fileName());
+ buttonPressed =
+ MOBase::TaskDialog(qApp->activeModalWidget(),
+ QObject::tr("INI file is read-only"))
+ .main(QObject::tr("INI file is read-only"))
+ .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" which "
+ "is currently set to read-only.")
+ .arg(fileInfo.fileName()))
+ .icon(QMessageBox::Warning)
+ .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes})
+ .button({QObject::tr("Allow the write once"),
+ QObject::tr("The file will be set to read-only again."),
+ QMessageBox::Ignore})
+ .button({QObject::tr("Skip this file"), QMessageBox::No})
+ .remember("clearReadOnly", fileInfo.fileName())
+ .exec();
+
+ if (buttonPressed & (QMessageBox::Yes | QMessageBox::Ignore)) {
+ file.setPermissions(file.permissions() | QFile::WriteUser);
+ }
+
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ reportError(tr("failed to write to %1").arg(editor->documentTitle()));
+ } else {
+ write = true;
+ }
+ }
+
+ if (write) {
+ file.write(editor->toPlainText().toUtf8().replace('\n', "\r\n"));
+ file.close();
+ }
+
+ if (buttonPressed == QMessageBox::Ignore) {
+ file.setPermissions(file.permissions() & ~(QFile::WriteUser));
+ }
+}
+
+void TextViewer::saveFile()
+{
+ QWidget* currentPage = m_EditorTabs->currentWidget();
+ QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView");
+ saveFile(editor);
+
+ m_Modified.erase(editor);
+}
+
+void TextViewer::modified()
+{
+ QWidget* currentPage = m_EditorTabs->currentWidget();
+ QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView");
+
+ m_Modified.insert(editor);
+}
+
+void TextViewer::addFile(const QString& fileName, bool writable)
+{
+ QFile file(fileName);
+ if (!file.open(QIODevice::ReadOnly)) {
+ throw Exception(tr("file not found: %1").arg(fileName));
+ }
+ QByteArray temp = file.readAll();
+
+ QWidget* page = new QWidget();
+ QVBoxLayout* layout = new QVBoxLayout(page);
+ QTextEdit* editor = new QTextEdit(page);
+ QTextDocument* document = new QTextDocument(page);
+ if (ui->showWhitespace->isChecked()) {
+ QTextOption option;
+ option.setFlags(QTextOption::ShowTabsAndSpaces);
+ document->setDefaultTextOption(option);
+ }
+ editor->setDocument(document);
+ editor->setAcceptRichText(false);
+ editor->setPlainText(QString(temp));
+ editor->setLineWrapMode(QTextEdit::NoWrap);
+ editor->setObjectName("editorView");
+ editor->setDocumentTitle(fileName);
+ editor->installEventFilter(this);
+ editor->setReadOnly(!writable);
+
+ // set text highlighting color in inactive window equal to text hightlighting color in
+ // active window
+ QPalette palette = editor->palette();
+ palette.setColor(QPalette::Inactive, QPalette::Highlight,
+ palette.color(QPalette::Active, QPalette::Highlight));
+ palette.setColor(QPalette::Inactive, QPalette::HighlightedText,
+ palette.color(QPalette::Active, QPalette::HighlightedText));
+ editor->setPalette(palette);
+
+ // add hotkeys for searching through the document
+ QAction* findAction = new QAction(QString("&Find"), editor);
+ findAction->setShortcut(QKeySequence::Find);
+ editor->addAction(findAction);
+ QAction* findNextAction = new QAction(QString("Find &Next"), editor);
+ findAction->setShortcut(QKeySequence::FindNext);
+ editor->addAction(findNextAction);
+
+ layout->addWidget(editor);
+ if (writable) {
+ QPushButton* saveBtn = new QPushButton(tr("Save"), page);
+ layout->addWidget(saveBtn);
+ connect(saveBtn, SIGNAL(clicked()), this, SLOT(saveFile()));
+ connect(editor, SIGNAL(textChanged()), this, SLOT(modified()));
+ }
+ page->setLayout(layout);
+ m_EditorTabs->addTab(page, QFileInfo(fileName).fileName());
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/textviewer.ui b/libs/uibase/src/textviewer.ui
new file mode 100644
index 0000000..e2af30e
--- /dev/null
+++ b/libs/uibase/src/textviewer.ui
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>TextViewer</class>
+ <widget class="QDialog" name="TextViewer">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>607</width>
+ <height>528</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Log Viewer</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="text">
+ <string>Placeholder</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showWhitespace">
+ <property name="text">
+ <string>Show Whitespace</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="editorTabs">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="currentIndex">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/uibase/src/tutorabledialog.cpp b/libs/uibase/src/tutorabledialog.cpp
new file mode 100644
index 0000000..61aad62
--- /dev/null
+++ b/libs/uibase/src/tutorabledialog.cpp
@@ -0,0 +1,40 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/tutorabledialog.h>
+
+namespace MOBase
+{
+
+TutorableDialog::TutorableDialog(const QString& name, QWidget* parent)
+ : QDialog(parent), m_Tutorial(this, name)
+{}
+
+void TutorableDialog::showEvent(QShowEvent* event)
+{
+ m_Tutorial.registerControl();
+ QDialog::showEvent(event);
+}
+
+void TutorableDialog::resizeEvent(QResizeEvent* event)
+{
+ m_Tutorial.resize(event->size());
+ QDialog::resizeEvent(event);
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/tutorialcontrol.cpp b/libs/uibase/src/tutorialcontrol.cpp
new file mode 100644
index 0000000..fee19f4
--- /dev/null
+++ b/libs/uibase/src/tutorialcontrol.cpp
@@ -0,0 +1,384 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/tutorialcontrol.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <uibase/tutorialmanager.h>
+#include <uibase/utility.h>
+
+#include <QAbstractButton>
+#include <QAction>
+#include <QBitmap>
+#include <QCoreApplication>
+#include <QDir>
+#include <QFile>
+#include <QGraphicsObject>
+#include <QImage>
+#include <QMenuBar>
+#include <QMouseEvent>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QQuickItem>
+#include <QTableWidget>
+#include <QTimer>
+#include <QToolBar>
+
+#include <QMetaEnum>
+#include <QMetaMethod>
+#include <QMetaObject>
+#include <qmetaobject.h>
+
+namespace MOBase
+{
+
+TutorialControl::TutorialControl(const TutorialControl& reference)
+ : QObject(reference.parent()), m_TargetControl(reference.m_TargetControl),
+ m_Name(reference.m_Name), m_TutorialView(nullptr),
+ m_Manager(TutorialManager::instance()), m_ExpectedTab(0),
+ m_CurrentClickControl(nullptr)
+{}
+
+TutorialControl::TutorialControl(QWidget* targetControl, const QString& name)
+ : QObject(nullptr), m_TargetControl(targetControl), m_Name(name),
+ m_TutorialView(nullptr), m_Manager(TutorialManager::instance()), m_ExpectedTab(0),
+ m_CurrentClickControl(nullptr)
+{}
+
+TutorialControl::~TutorialControl()
+{
+ m_Manager.unregisterControl(m_Name);
+ finish();
+}
+
+void TutorialControl::registerControl()
+{
+ m_Manager.registerControl(m_Name, this);
+}
+
+void TutorialControl::resize(const QSize& size)
+{
+ if (m_TutorialView != nullptr) {
+ m_TutorialView->resize(size.width(), size.height());
+ }
+}
+
+void TutorialControl::expose(const QString& widgetName, QObject* widget)
+{
+ m_ExposedObjects.push_back(std::make_pair(widgetName, widget));
+}
+
+static QString canonicalPath(const QString& path)
+{
+#ifdef _WIN32
+ std::unique_ptr<wchar_t[]> buffer(new wchar_t[32768]);
+ DWORD res = ::GetShortPathNameW((wchar_t*)path.utf16(), buffer.get(), 32768);
+ if (res == 0) {
+ return path;
+ }
+ res = ::GetLongPathNameW(buffer.get(), buffer.get(), 32768);
+ if (res == 0) {
+ return path;
+ }
+ return QString::fromWCharArray(buffer.get());
+#else
+ QFileInfo fi(path);
+ QString canonical = fi.canonicalFilePath();
+ return canonical.isEmpty() ? path : canonical;
+#endif
+}
+
+void TutorialControl::startTutorial(const QString& tutorial)
+{
+ if (m_TutorialView == nullptr) {
+ m_TutorialView = new QQuickWidget(m_TargetControl);
+ m_TutorialView->setResizeMode(QQuickWidget::SizeRootObjectToView);
+ m_TutorialView->setAttribute(Qt::WA_TranslucentBackground);
+ m_TutorialView->setAttribute(Qt::WA_AlwaysStackOnTop);
+ m_TutorialView->setClearColor(Qt::transparent);
+ m_TutorialView->setStyleSheet("background: transparent");
+ m_TutorialView->setObjectName("tutorialView");
+ m_TutorialView->rootContext()->setContextProperty("manager", &m_Manager);
+
+ QString qmlName =
+ canonicalPath(QCoreApplication::applicationDirPath() + "/tutorials") +
+ "/tutorials_" + m_Name.toLower() + ".qml";
+ QUrl qmlSource = QUrl::fromLocalFile(qmlName);
+
+ m_TutorialView->setSource(qmlSource);
+ m_TutorialView->resize(m_TargetControl->width(), m_TargetControl->height());
+ m_TutorialView->rootContext()->setContextProperty("scriptName", tutorial);
+ m_TutorialView->rootContext()->setContextProperty("tutorialControl", this);
+ m_TutorialView->rootContext()->setContextProperty("applicationWindow",
+ m_TargetControl);
+ m_TutorialView->rootContext()->setContextProperty("organizer",
+ m_Manager.organizerCore());
+
+ for (std::vector<std::pair<QString, QObject*>>::const_iterator iter =
+ m_ExposedObjects.begin();
+ iter != m_ExposedObjects.end(); ++iter) {
+ m_TutorialView->rootContext()->setContextProperty(iter->first, iter->second);
+ }
+ m_TutorialView->show();
+ m_TutorialView->raise();
+ if (!QMetaObject::invokeMethod(m_TutorialView->rootObject(), "init")) {
+ reportError(tr(
+ "Tutorial failed to start, please check \"mo_interface.log\" for details."));
+ m_TutorialView->close();
+ }
+ }
+}
+
+void TutorialControl::lockUI(bool locked)
+{
+ m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, !locked);
+
+ QMetaObject::invokeMethod(m_TutorialView->rootObject(), "enableBackground",
+ Q_ARG(QVariant, QVariant(locked)));
+}
+
+void TutorialControl::simulateClick(int x, int y)
+{
+ bool wasTransparent = m_TutorialView->testAttribute(Qt::WA_TransparentForMouseEvents);
+ if (!wasTransparent) {
+ m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, true);
+ }
+ QWidget* hitControl = m_TargetControl->childAt(x, y);
+ QPoint globalPos = m_TargetControl->mapToGlobal(QPoint(x, y));
+ QPoint hitPos = hitControl->mapFromGlobal(globalPos);
+ QMouseEvent* downEvent = new QMouseEvent(
+ QEvent::MouseButtonPress, QPointF(hitPos), QPointF(globalPos),
+ Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+ QMouseEvent* upEvent =
+ new QMouseEvent(QEvent::MouseButtonRelease, QPointF(hitPos), QPointF(globalPos),
+ Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+
+ qApp->postEvent(hitControl, (QEvent*)downEvent);
+ qApp->postEvent(hitControl, (QEvent*)upEvent);
+
+ if (!wasTransparent) {
+ m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, false);
+ }
+}
+
+QWidget* TutorialControl::getChild(const QString& name)
+{
+ if (m_TargetControl != nullptr) {
+ return m_TargetControl->findChild<QWidget*>(name);
+ } else {
+ return nullptr;
+ }
+}
+
+void TutorialControl::finish()
+{
+ if (m_TutorialView != nullptr) {
+ m_TutorialView->deleteLater();
+ }
+ m_TutorialView = nullptr;
+ m_TutorialView = nullptr;
+}
+
+QRect TutorialControl::getRect(const QString& widgetName)
+{
+ if (m_TargetControl != nullptr) {
+ QWidget* widget = m_TargetControl->findChild<QWidget*>(widgetName);
+ if (widget != nullptr) {
+ QRect res = widget->rect();
+ QPoint pos = widget->mapTo(m_TargetControl, res.topLeft());
+ res.moveTopLeft(pos);
+ return res;
+ } else {
+ log::error("{} not found", widgetName);
+ return QRect();
+ }
+ } else {
+ return QRect();
+ }
+}
+
+QRect TutorialControl::getActionRect(const QString& widgetName)
+{
+ if (m_TargetControl != nullptr) {
+ QToolBar* toolBar = m_TargetControl->findChild<QToolBar*>("toolBar");
+ foreach (QAction* action, toolBar->actions()) {
+ if (action->objectName() == widgetName) {
+ return toolBar->actionGeometry(action);
+ }
+ }
+ }
+ return QRect();
+}
+
+QRect TutorialControl::getMenuRect(const QString&)
+{
+ if (m_TargetControl != nullptr) {
+ QMenuBar* menuBar = m_TargetControl->findChild<QMenuBar*>("menuBar");
+ return menuBar->geometry();
+ }
+ return QRect();
+}
+
+void TutorialControl::nextTutorialStepProxy()
+{
+
+ if (m_TutorialView != nullptr) {
+ QObject* background = m_TutorialView->rootObject();
+
+ QTimer::singleShot(1, background, SLOT(nextStep()));
+ lockUI(true);
+
+ bool success = true;
+ for (QMetaObject::Connection connection : m_Connections) {
+ if (!disconnect(connection)) {
+ success = false;
+ }
+ }
+ m_Connections.clear();
+ if (!success) {
+ log::error("failed to disconnect tutorial proxy");
+ }
+ } else {
+ log::error("failed to proceed to next tutorial step");
+ finish();
+ }
+}
+
+void TutorialControl::tabChangedProxy(int selected)
+{
+ if ((m_TutorialView != nullptr) && (selected == m_ExpectedTab)) {
+ QObject* background = m_TutorialView->rootObject();
+ QTimer::singleShot(1, background, SLOT(nextStep()));
+ lockUI(true);
+ bool success = true;
+ for (QMetaObject::Connection connection : m_Connections) {
+ if (!disconnect(connection)) {
+ success = false;
+ }
+ }
+ m_Connections.clear();
+ if (!success) {
+ log::error("failed to disconnect tab-changed proxy");
+ }
+ }
+}
+
+bool TutorialControl::waitForAction(const QString& actionName)
+{
+ if (m_TargetControl != nullptr) {
+ QAction* action = m_TargetControl->findChild<QAction*>(actionName);
+ if (action == nullptr) {
+ log::error("no action \"{}\" in control \"{}\"", actionName, m_Name);
+ return false;
+ }
+ if (action->isEnabled()) {
+ if (action->menu() != nullptr) {
+ m_Connections.append(connect(action->menu(), SIGNAL(aboutToShow()), this,
+ SLOT(nextTutorialStepProxy())));
+ } else {
+ m_Connections.append(
+ connect(action, SIGNAL(triggered()), this, SLOT(nextTutorialStepProxy())));
+ }
+ lockUI(false);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+bool TutorialControl::waitForButton(const QString& buttonName)
+{
+ if (m_TargetControl != nullptr) {
+ QAbstractButton* button = m_TargetControl->findChild<QAbstractButton*>(buttonName);
+ if (button == nullptr) {
+ log::error("no button \"{}\" in control \"{}\"", buttonName, m_Name);
+ return false;
+ }
+ if (button->isEnabled()) {
+ m_Connections.append(
+ connect(button, SIGNAL(pressed()), this, SLOT(nextTutorialStepProxy())));
+ lockUI(false);
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+bool TutorialControl::waitForTabOpen(const QString& tabControlName, const QString& tab)
+{
+ if (m_TargetControl != nullptr) {
+ QTabWidget* tabWidget = m_TargetControl->findChild<QTabWidget*>(tabControlName);
+ if (tabWidget == nullptr) {
+ log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name);
+ return false;
+ }
+ if (tabWidget->findChild<QWidget*>(tab) == nullptr) {
+ log::error("no widget \"{}\" found in tab widget \"{}\"", tab, tabControlName);
+ return false;
+ }
+ int tabIndex = tabWidget->indexOf(tabWidget->findChild<QWidget*>(tab));
+ if (tabIndex == -1) {
+ log::error("widget \"{}\" does not appear to be a tab in tab widget \"{}\"", tab,
+ tabControlName);
+ return false;
+ }
+ if (tabWidget->isEnabled()) {
+ if (tabWidget->currentIndex() != tabIndex) {
+ m_ExpectedTab = tabIndex;
+ m_Connections.append(connect(tabWidget, SIGNAL(currentChanged(int)), this,
+ SLOT(tabChangedProxy(int))));
+ lockUI(false);
+ } else {
+ QObject* background = m_TutorialView->rootObject();
+ QTimer::singleShot(1, background, SLOT(nextStep()));
+ lockUI(true);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+}
+
+const QString TutorialControl::getTabName(const QString& tabControlName)
+{
+ if (m_TargetControl != nullptr) {
+ QTabWidget* tabWidget = m_TargetControl->findChild<QTabWidget*>(tabControlName);
+ if (tabWidget == nullptr) {
+ log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name);
+ return QString();
+ }
+ if (tabWidget->currentIndex() == -1) {
+ return QString();
+ } else {
+ return tabWidget->currentWidget()->objectName();
+ }
+ } else {
+ return QString();
+ }
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/tutorialmanager.cpp b/libs/uibase/src/tutorialmanager.cpp
new file mode 100644
index 0000000..58191e7
--- /dev/null
+++ b/libs/uibase/src/tutorialmanager.cpp
@@ -0,0 +1,112 @@
+/*
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <uibase/tutorialmanager.h>
+#include <uibase/log.h>
+#include <uibase/tutorialcontrol.h>
+#include <uibase/utility.h>
+#include <QApplication>
+#include <QDir>
+#include <QList>
+#include <QString>
+#include <QTimer>
+
+namespace MOBase
+{
+
+TutorialManager* TutorialManager::s_Instance = nullptr;
+
+TutorialManager::TutorialManager(const QString& tutorialPath, QObject* organizerCore)
+ : m_TutorialPath(tutorialPath), m_OrganizerCore(organizerCore)
+{}
+
+void TutorialManager::init(const QString& tutorialPath, QObject* organizerCore)
+{
+ if (s_Instance != nullptr) {
+ delete s_Instance;
+ }
+ s_Instance = new TutorialManager(tutorialPath, organizerCore);
+}
+
+TutorialManager& TutorialManager::instance()
+{
+ if (s_Instance == nullptr) {
+ throw Exception(tr("tutorial manager not set up yet"));
+ }
+ return *s_Instance;
+}
+
+void TutorialManager::activateTutorial(const QString& windowName,
+ const QString& tutorialName)
+{
+ std::map<QString, TutorialControl*>::iterator iter = m_Controls.find(windowName);
+ if (iter != m_Controls.end()) {
+ // control already visible, start tutorial right away
+ iter->second->startTutorial(m_TutorialPath + tutorialName);
+ } else {
+ m_PendingTutorials[windowName] = tutorialName;
+ }
+}
+
+void TutorialManager::finishWindowTutorial(const QString& windowName)
+{
+ emit windowTutorialFinished(windowName);
+ // QSettings &settings = Settings::instance().directInterface();
+ // settings.setValue(QString("CompletedWindowTutorials/") + windowName, true);
+}
+
+bool TutorialManager::hasTutorial(const QString& tutorialName)
+{
+ return QFile::exists(m_TutorialPath + tutorialName);
+}
+
+QWidget* TutorialManager::findControl(const QString& controlName)
+{
+ QWidget* mainWindow = qApp->activeWindow();
+ if (mainWindow != nullptr) {
+ return mainWindow->findChild<QWidget*>(controlName);
+ } else {
+ return nullptr;
+ }
+}
+
+void TutorialManager::registerControl(const QString& windowName,
+ TutorialControl* control)
+{
+ m_Controls[windowName] = control;
+ std::map<QString, QString>::iterator iter = m_PendingTutorials.find(windowName);
+ if (iter != m_PendingTutorials.end()) {
+ // there is a pending tutorial for this window, display it
+ QTimer::singleShot(0, [control, tutorial = m_TutorialPath + iter->second] {
+ control->startTutorial(tutorial);
+ });
+ m_PendingTutorials.erase(iter);
+ }
+}
+
+void TutorialManager::unregisterControl(const QString& windowName)
+{
+ std::map<QString, TutorialControl*>::iterator iter = m_Controls.find(windowName);
+ if (iter != m_Controls.end()) {
+ m_Controls.erase(iter);
+ } else {
+ log::warn("failed to remove tutorial control {}", windowName);
+ }
+}
+} // namespace MOBase
diff --git a/libs/uibase/src/uibase_en.ts b/libs/uibase/src/uibase_en.ts
new file mode 100644
index 0000000..defc545
--- /dev/null
+++ b/libs/uibase/src/uibase_en.ts
@@ -0,0 +1,407 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>FilterWidget</name>
+ <message>
+ <location filename="filterwidget.cpp" line="610"/>
+ <source>Filter options</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="617"/>
+ <source>Use regular expressions</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="618"/>
+ <source>Use regular expressions in filters</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="627"/>
+ <source>Case sensitive</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="629"/>
+ <source>Make regular expressions case sensitive (/i)</source>
+ <extracomment>leave &quot;(/i)&quot; verbatim</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="639"/>
+ <source>Extended</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="641"/>
+ <source>Ignores unescaped whitespace in regular expressions (/x)</source>
+ <extracomment>leave &quot;(/x)&quot; verbatim</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="651"/>
+ <source>Keep selection in view</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="filterwidget.cpp" line="652"/>
+ <source>Scroll to keep the current selection in view after filtering</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>FindDialog</name>
+ <message>
+ <location filename="finddialog.ui" line="14"/>
+ <source>Find</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="24"/>
+ <source>Find what:</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="31"/>
+ <location filename="finddialog.ui" line="34"/>
+ <source>Search term</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="47"/>
+ <location filename="finddialog.ui" line="50"/>
+ <source>Find next occurence from current file position.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="53"/>
+ <source>&amp;Find Next</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="finddialog.ui" line="60"/>
+ <location filename="finddialog.ui" line="63"/>
+ <location filename="finddialog.ui" line="66"/>
+ <source>Close</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="filterwidget.cpp" line="128"/>
+ <source>Filter</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginrequirements.cpp" line="31"/>
+ <source>One of the following plugins must be enabled: %1.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="pluginrequirements.cpp" line="34"/>
+ <source>This plugin can only be enabled if the &apos;%1&apos; plugin is installed and enabled.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message numerus="yes">
+ <location filename="pluginrequirements.cpp" line="63"/>
+ <source>This plugin can only be enabled for the following game(s): %1.</source>
+ <translation type="unfinished">
+ <numerusform></numerusform>
+ <numerusform></numerusform>
+ </translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="43"/>
+ <location filename="registry.cpp" line="44"/>
+ <location filename="textviewer.cpp" line="173"/>
+ <location filename="textviewer.cpp" line="174"/>
+ <source>INI file is read-only</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="45"/>
+ <location filename="textviewer.cpp" line="175"/>
+ <source>Mod Organizer is attempting to write to &quot;%1&quot; which is currently set to read-only.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="49"/>
+ <location filename="textviewer.cpp" line="179"/>
+ <source>Clear the read-only flag</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="50"/>
+ <location filename="textviewer.cpp" line="180"/>
+ <source>Allow the write once</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="51"/>
+ <location filename="textviewer.cpp" line="181"/>
+ <source>The file will be set to read-only again.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="registry.cpp" line="53"/>
+ <location filename="textviewer.cpp" line="183"/>
+ <source>Skip this file</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="65"/>
+ <location filename="report.cpp" line="71"/>
+ <source>Error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="338"/>
+ <source>You can reset these choices by clicking &quot;Reset Dialog Choices&quot; in the General tab of the Settings</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="347"/>
+ <source>Always ask</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="348"/>
+ <location filename="report.cpp" line="356"/>
+ <source>Remember my choice</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="report.cpp" line="350"/>
+ <source>Remember my choice for %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="safewritefile.cpp" line="40"/>
+ <source>Failed to save &apos;%1&apos;, could not create a temporary file: %2 (error %3)</source>
+ <oldsource>Failed to save &apos;{}&apos;, could not create a temporary file: {} (error {})</oldsource>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="62"/>
+ <source>removal of &quot;%1&quot; failed: %2</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="71"/>
+ <source>removal of &quot;%1&quot; failed</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="75"/>
+ <source>&quot;%1&quot; doesn&apos;t exist (remove)</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="350"/>
+ <source>Error %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="534"/>
+ <location filename="utility.cpp" line="557"/>
+ <source>You have an invalid custom browser command in the settings.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="673"/>
+ <location filename="utility.cpp" line="702"/>
+ <source>failed to create directory &quot;%1&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="682"/>
+ <location filename="utility.cpp" line="709"/>
+ <source>failed to copy &quot;%1&quot; to &quot;%2&quot;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1134"/>
+ <source>%1 B</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1134"/>
+ <source>%1 KB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1135"/>
+ <source>%1 MB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1135"/>
+ <source>%1 GB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1136"/>
+ <source>%1 TB</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1141"/>
+ <source>%1 B/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1141"/>
+ <source>%1 KB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1142"/>
+ <source>%1 MB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1142"/>
+ <source>%1 GB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1143"/>
+ <source>%1 TB/s</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QuestionBoxMemory</name>
+ <message>
+ <location filename="questionboxmemory.ui" line="106"/>
+ <source>Remember selection</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="questionboxmemory.ui" line="113"/>
+ <source>Remember selection only for %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TaskDialog</name>
+ <message>
+ <location filename="taskdialog.ui" line="20"/>
+ <source>Dialog</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="56"/>
+ <source>icon</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="102"/>
+ <source>dummy main text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="146"/>
+ <source>dummy content text</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="206"/>
+ <source>dummy button</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="234"/>
+ <source>dummy checkbox</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="taskdialog.ui" line="320"/>
+ <source>Details</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TextViewer</name>
+ <message>
+ <location filename="textviewer.ui" line="14"/>
+ <source>Log Viewer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.ui" line="22"/>
+ <source>Placeholder</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.ui" line="29"/>
+ <source>Show Whitespace</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="60"/>
+ <source>Save changes?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="61"/>
+ <source>Do you want to save changes to %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="192"/>
+ <source>failed to write to %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="229"/>
+ <source>file not found: %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="textviewer.cpp" line="270"/>
+ <source>Save</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TutorialControl</name>
+ <message>
+ <location filename="tutorialcontrol.cpp" line="135"/>
+ <source>Tutorial failed to start, please check &quot;mo_interface.log&quot; for details.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>TutorialManager</name>
+ <message>
+ <location filename="tutorialmanager.cpp" line="50"/>
+ <source>tutorial manager not set up yet</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>uibase</name>
+ <message>
+ <location filename="utility.cpp" line="1205"/>
+ <source>h</source>
+ <extracomment>Time remaining hours</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1208"/>
+ <source>m</source>
+ <extracomment>Time remaining minutes</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="utility.cpp" line="1211"/>
+ <source>s</source>
+ <extracomment>Time remaining seconds</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp
new file mode 100644
index 0000000..0ad64ad
--- /dev/null
+++ b/libs/uibase/src/utility.cpp
@@ -0,0 +1,1260 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/utility.h>
+#include <uibase/log.h>
+#include <uibase/report.h>
+#include <QApplication>
+#include <QBuffer>
+#include <QCollator>
+#include <QDesktopServices>
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QImage>
+#include <QImageReader>
+#include <QLayout>
+#include <QProcess>
+#include <QRegularExpression>
+#include <QScreen>
+#include <QStandardPaths>
+#include <QStringEncoder>
+#include <QUrl>
+#include <QUuid>
+#include <QtDebug>
+#include <cerrno>
+#include <cstring>
+#include <format>
+#include <iostream>
+#include <memory>
+#include <sstream>
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <Windows.h>
+#define FO_RECYCLE 0x1003
+#endif
+
+namespace MOBase
+{
+
+bool removeDir(const QString& dirName)
+{
+ QDir dir(dirName);
+
+ if (dir.exists()) {
+ Q_FOREACH (QFileInfo info,
+ dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
+ QDir::AllDirs | QDir::Files,
+ QDir::DirsFirst)) {
+ if (info.isDir()) {
+ if (!removeDir(info.absoluteFilePath())) {
+ return false;
+ }
+ } else {
+ // On Linux, just make sure file is writable before removing
+ QFile file(info.absoluteFilePath());
+ file.setPermissions(file.permissions() | QFile::WriteUser);
+ if (!file.remove()) {
+ reportError(QObject::tr("removal of \"%1\" failed: %2")
+ .arg(info.absoluteFilePath())
+ .arg(file.errorString()));
+ return false;
+ }
+ }
+ }
+
+ if (!dir.rmdir(dirName)) {
+ reportError(QObject::tr("removal of \"%1\" failed").arg(dir.absolutePath()));
+ return false;
+ }
+ } else {
+ reportError(QObject::tr("\"%1\" doesn't exist (remove)").arg(dirName));
+ return false;
+ }
+
+ return true;
+}
+
+bool copyDir(const QString& sourceName, const QString& destinationName, bool merge)
+{
+ QDir sourceDir(sourceName);
+ if (!sourceDir.exists()) {
+ return false;
+ }
+ QDir destDir(destinationName);
+ if (!destDir.exists()) {
+ destDir.mkdir(destinationName);
+ } else if (!merge) {
+ return false;
+ }
+
+ QStringList files = sourceDir.entryList(QDir::Files);
+ foreach (QString fileName, files) {
+ QString srcName = sourceName + "/" + fileName;
+ QString destName = destinationName + "/" + fileName;
+ QFile::copy(srcName, destName);
+ }
+
+ files.clear();
+ // we leave out symlinks because that could cause an endless recursion
+ QStringList subDirs =
+ sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
+ foreach (QString subDir, subDirs) {
+ QString srcName = sourceName + "/" + subDir;
+ QString destName = destinationName + "/" + subDir;
+ copyDir(srcName, destName, merge);
+ }
+ return true;
+}
+
+// Linux shell operations use QFile/QDir instead of SHFileOperation
+
+static bool shellOpCopy(const QStringList& sourceNames,
+ const QStringList& destinationNames)
+{
+ // Multiple sources → single destination: treat destination as a directory
+ if (destinationNames.count() == 1 && sourceNames.count() > 1) {
+ QDir destDir(destinationNames[0]);
+ if (!destDir.exists()) {
+ destDir.mkpath(".");
+ }
+ for (const auto& src : sourceNames) {
+ QFileInfo srcInfo(src);
+ QString dest = destinationNames[0] + "/" + srcInfo.fileName();
+ if (!QFile::copy(src, dest)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // 1:1 or N:N — direct file-to-file copy
+ if (destinationNames.count() != sourceNames.count()) {
+ return false;
+ }
+
+ for (int i = 0; i < sourceNames.count(); ++i) {
+ QFileInfo destInfo(destinationNames[i]);
+ if (!destInfo.dir().exists()) {
+ destInfo.dir().mkpath(".");
+ }
+ if (!QFile::copy(sourceNames[i], destinationNames[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool shellOpMove(const QStringList& sourceNames,
+ const QStringList& destinationNames)
+{
+ // Multiple sources → single destination: treat destination as a directory
+ if (destinationNames.count() == 1 && sourceNames.count() > 1) {
+ QDir destDir(destinationNames[0]);
+ if (!destDir.exists()) {
+ destDir.mkpath(".");
+ }
+ for (const auto& src : sourceNames) {
+ QFileInfo srcInfo(src);
+ QString dest = destinationNames[0] + "/" + srcInfo.fileName();
+ if (!QFile::rename(src, dest)) {
+ if (!QFile::copy(src, dest) || !QFile::remove(src)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // 1:1 or N:N — direct file-to-file move
+ if (destinationNames.count() != sourceNames.count()) {
+ return false;
+ }
+
+ for (int i = 0; i < sourceNames.count(); ++i) {
+ QFileInfo destInfo(destinationNames[i]);
+ if (!destInfo.dir().exists()) {
+ destInfo.dir().mkpath(".");
+ }
+ if (!QFile::rename(sourceNames[i], destinationNames[i])) {
+ if (!QFile::copy(sourceNames[i], destinationNames[i]) ||
+ !QFile::remove(sourceNames[i])) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool shellOpDelete(const QStringList& fileNames, bool recycle)
+{
+ // On Linux, "recycle" moves to trash using Qt; otherwise just delete
+ for (const auto& fileName : fileNames) {
+ QFileInfo fi(fileName);
+ if (fi.isDir()) {
+ if (!removeDir(fileName)) {
+ return false;
+ }
+ } else {
+ if (recycle) {
+ if (!QFile::moveToTrash(fileName)) {
+ return false;
+ }
+ } else {
+ if (!QFile::remove(fileName)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool shellCopy(const QStringList& sourceNames, const QStringList& destinationNames,
+ QWidget* dialog)
+{
+ (void)dialog;
+ return shellOpCopy(sourceNames, destinationNames);
+}
+
+bool shellCopy(const QString& sourceNames, const QString& destinationNames,
+ bool yesToAll, QWidget* dialog)
+{
+ (void)yesToAll;
+ (void)dialog;
+ return shellOpCopy(QStringList() << sourceNames, QStringList() << destinationNames);
+}
+
+bool shellMove(const QStringList& sourceNames, const QStringList& destinationNames,
+ QWidget* dialog)
+{
+ (void)dialog;
+ return shellOpMove(sourceNames, destinationNames);
+}
+
+bool shellMove(const QString& sourceNames, const QString& destinationNames,
+ bool yesToAll, QWidget* dialog)
+{
+ (void)yesToAll;
+ (void)dialog;
+ return shellOpMove(QStringList() << sourceNames, QStringList() << destinationNames);
+}
+
+bool shellRename(const QString& oldName, const QString& newName, bool yesToAll,
+ QWidget* dialog)
+{
+ (void)yesToAll;
+ (void)dialog;
+ return QFile::rename(oldName, newName);
+}
+
+bool shellDelete(const QStringList& fileNames, bool recycle, QWidget* dialog)
+{
+ (void)dialog;
+ return shellOpDelete(fileNames, recycle);
+}
+
+namespace shell
+{
+
+ static QString g_urlHandler;
+
+ Result::Result(bool success, DWORD error, QString message, HANDLE process)
+ : m_success(success), m_error(error), m_message(std::move(message)),
+ m_process(process)
+ {
+ if (m_message.isEmpty()) {
+ m_message = QString::fromStdWString(formatSystemMessage(m_error));
+ }
+ }
+
+ Result Result::makeFailure(DWORD error, QString message)
+ {
+ return Result(false, error, std::move(message), INVALID_HANDLE_VALUE);
+ }
+
+ Result Result::makeSuccess(HANDLE process)
+ {
+ return Result(true, ERROR_SUCCESS, {}, process);
+ }
+
+ bool Result::success() const
+ {
+ return m_success;
+ }
+
+ Result::operator bool() const
+ {
+ return m_success;
+ }
+
+ DWORD Result::error()
+ {
+ return m_error;
+ }
+
+ const QString& Result::message() const
+ {
+ return m_message;
+ }
+
+ HANDLE Result::processHandle() const
+ {
+ return m_process.get();
+ }
+
+ HANDLE Result::stealProcessHandle()
+ {
+ const auto h = m_process.release();
+ m_process.reset(INVALID_HANDLE_VALUE);
+ return h;
+ }
+
+ QString Result::toString() const
+ {
+ if (m_message.isEmpty()) {
+ return QObject::tr("Error %1").arg(m_error);
+ } else {
+ return m_message;
+ }
+ }
+
+ QString formatError(int i)
+ {
+ switch (i) {
+ case 0:
+ return "The operating system is out of memory or resources";
+ case (int)ERROR_FILE_NOT_FOUND:
+ return "The specified file was not found";
+ case (int)ERROR_PATH_NOT_FOUND:
+ return "The specified path was not found";
+ case (int)ERROR_BAD_FORMAT:
+ return "The .exe file is invalid (non-Win32 .exe or error in .exe image)";
+ case SE_ERR_ACCESSDENIED:
+ return "The operating system denied access to the specified file";
+ case SE_ERR_ASSOCINCOMPLETE:
+ return "The file name association is incomplete or invalid";
+ case SE_ERR_DDEBUSY:
+ return "The DDE transaction could not be completed because other DDE "
+ "transactions were being processed";
+ case SE_ERR_DDEFAIL:
+ return "The DDE transaction failed";
+ case SE_ERR_DDETIMEOUT:
+ return "The DDE transaction could not be completed because the request "
+ "timed out";
+ case SE_ERR_DLLNOTFOUND:
+ return "The specified DLL was not found";
+ case SE_ERR_NOASSOC:
+ return "There is no application associated with the given file name "
+ "extension";
+ case SE_ERR_OOM:
+ return "There was not enough memory to complete the operation";
+ case SE_ERR_SHARE:
+ return "A sharing violation occurred";
+ default:
+ return QString("Unknown error %1").arg(i);
+ }
+ }
+
+ Result ExploreDirectory(const QFileInfo& info)
+ {
+ const auto path = info.absoluteFilePath();
+ // Use xdg-open on Linux
+ if (QProcess::startDetached("xdg-open", {path})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open directory"));
+ }
+
+ Result ExploreFileInDirectory(const QFileInfo& info)
+ {
+ // Try to use the system file manager to highlight the file
+ // dbus method for Nautilus/Files, fallback to xdg-open on parent dir
+ const auto dir = info.absolutePath();
+ if (QProcess::startDetached("xdg-open", {dir})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file manager"));
+ }
+
+ Result Explore(const QFileInfo& info)
+ {
+ if (info.isFile()) {
+ return ExploreFileInDirectory(info);
+ } else if (info.isDir()) {
+ return ExploreDirectory(info);
+ } else {
+ const auto parent = info.dir();
+ if (parent.exists()) {
+ return ExploreDirectory(QFileInfo(parent.absolutePath()));
+ } else {
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND);
+ }
+ }
+ }
+
+ Result Explore(const QString& path)
+ {
+ return Explore(QFileInfo(path));
+ }
+
+ Result Explore(const QDir& dir)
+ {
+ return Explore(QFileInfo(dir.absolutePath()));
+ }
+
+ Result Open(const QString& path)
+ {
+ if (QProcess::startDetached("xdg-open", {path})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file"));
+ }
+
+ Result Open(const QUrl& url)
+ {
+ log::debug("opening url '{}'", url.toString());
+
+ if (g_urlHandler.isEmpty()) {
+ if (QProcess::startDetached("xdg-open", {url.toString()})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open URL"));
+ } else {
+ // Custom URL handler
+ QString cmd = g_urlHandler;
+ cmd.replace("%1", url.toString());
+ if (QProcess::startDetached("/bin/sh", {"-c", cmd})) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND,
+ QObject::tr("You have an invalid custom browser command in the settings."));
+ }
+ }
+
+ Result Execute(const QString& program, const QString& params)
+ {
+ QStringList args;
+ if (!params.isEmpty()) {
+ args = QProcess::splitCommand(params);
+ }
+ if (QProcess::startDetached(program, args)) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to execute program"));
+ }
+
+ void SetUrlHandler(const QString& cmd)
+ {
+ g_urlHandler = cmd;
+ }
+
+ Result Delete(const QFileInfo& path)
+ {
+ if (QFile::remove(path.absoluteFilePath())) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_ACCESS_DENIED);
+ }
+
+ Result Rename(const QFileInfo& src, const QFileInfo& dest)
+ {
+ return Rename(src, dest, true);
+ }
+
+ Result Rename(const QFileInfo& src, const QFileInfo& dest, bool copyAllowed)
+ {
+ if (QFile::rename(src.absoluteFilePath(), dest.absoluteFilePath())) {
+ return Result::makeSuccess();
+ }
+
+ if (copyAllowed) {
+ if (QFile::copy(src.absoluteFilePath(), dest.absoluteFilePath())) {
+ QFile::remove(src.absoluteFilePath());
+ return Result::makeSuccess();
+ }
+ }
+
+ return Result::makeFailure(ERROR_ACCESS_DENIED);
+ }
+
+ Result CreateDirectories(const QDir& dir)
+ {
+ if (dir.mkpath(".")) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_ACCESS_DENIED,
+ QObject::tr("Failed to create directory: %1").arg(dir.path()));
+ }
+
+ Result DeleteDirectoryRecursive(const QDir& dir)
+ {
+ if (shellOpDelete({dir.path()}, false)) {
+ return Result::makeSuccess();
+ }
+ return Result::makeFailure(ERROR_ACCESS_DENIED);
+ }
+
+} // namespace shell
+
+bool moveFileRecursive(const QString& source, const QString& baseDir,
+ const QString& destination)
+{
+ QStringList pathComponents = destination.split("/");
+ QString path = baseDir;
+ for (QStringList::Iterator iter = pathComponents.begin();
+ iter != pathComponents.end() - 1; ++iter) {
+ path.append("/").append(*iter);
+ if (!QDir(path).exists() && !QDir().mkdir(path)) {
+ reportError(QObject::tr("failed to create directory \"%1\"").arg(path));
+ return false;
+ }
+ }
+
+ QString destinationAbsolute = baseDir.mid(0).append("/").append(destination);
+ if (!QFile::rename(source, destinationAbsolute)) {
+ // move failed, try copy & delete
+ if (!QFile::copy(source, destinationAbsolute)) {
+ reportError(QObject::tr("failed to copy \"%1\" to \"%2\"")
+ .arg(source)
+ .arg(destinationAbsolute));
+ return false;
+ } else {
+ QFile::remove(source);
+ }
+ }
+ return true;
+}
+
+bool copyFileRecursive(const QString& source, const QString& baseDir,
+ const QString& destination)
+{
+ QStringList pathComponents = destination.split("/");
+ QString path = baseDir;
+ for (QStringList::Iterator iter = pathComponents.begin();
+ iter != pathComponents.end() - 1; ++iter) {
+ path.append("/").append(*iter);
+ if (!QDir(path).exists() && !QDir().mkdir(path)) {
+ reportError(QObject::tr("failed to create directory \"%1\"").arg(path));
+ return false;
+ }
+ }
+
+ QString destinationAbsolute = baseDir.mid(0).append("/").append(destination);
+ if (!QFile::copy(source, destinationAbsolute)) {
+ reportError(QObject::tr("failed to copy \"%1\" to \"%2\"")
+ .arg(source)
+ .arg(destinationAbsolute));
+ return false;
+ }
+ return true;
+}
+
+std::wstring ToWString(const QString& source)
+{
+ return source.toStdWString();
+}
+
+std::string ToString(const QString& source, bool utf8)
+{
+ QByteArray array8bit;
+ if (utf8) {
+ array8bit = source.toUtf8();
+ } else {
+ array8bit = source.toLocal8Bit();
+ }
+ return std::string(array8bit.constData());
+}
+
+QString ToQString(const std::string& source)
+{
+ return QString::fromStdString(source);
+}
+
+QString ToQString(const std::wstring& source)
+{
+ return QString::fromStdWString(source);
+}
+
+bool isWindowsDrivePath(const QString& path)
+{
+ static const QRegularExpression re(R"(^\s*[A-Za-z]:[\\/].*)");
+ return re.match(path).hasMatch();
+}
+
+bool isWineZDrivePath(const QString& path)
+{
+ static const QRegularExpression re(R"(^\s*[Zz]:[\\/].*)");
+ return re.match(path).hasMatch();
+}
+
+QString toWinePath(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ QString p = path.trimmed();
+ if (p.isEmpty()) {
+ return p;
+ }
+
+ if (isWindowsDrivePath(p)) {
+ p.replace('/', '\\');
+ return p;
+ }
+
+ // Expand "~" on Linux before mapping to Z:.
+ if (p.startsWith("~")) {
+ p.replace(0, 1, QDir::homePath());
+ }
+
+ if (!QDir::isAbsolutePath(p)) {
+ p = QDir(p).absolutePath();
+ }
+
+ p = QDir::cleanPath(p);
+ p.replace('/', '\\');
+ return "Z:" + p;
+#endif
+}
+
+QString fromWinePath(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ QString p = path.trimmed();
+ if (p.isEmpty()) {
+ return p;
+ }
+
+ if (!isWindowsDrivePath(p)) {
+ return QDir::cleanPath(p);
+ }
+
+ // We only map Wine's Z: drive to host Linux absolute paths.
+ if (!isWineZDrivePath(p)) {
+ return p;
+ }
+
+ p = p.mid(2); // strip "Z:"
+ p.replace('\\', '/');
+ if (!p.startsWith('/')) {
+ p.prepend('/');
+ }
+
+ return QDir::cleanPath(p);
+#endif
+}
+
+QString normalizePathForHost(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ if (isWineZDrivePath(path)) {
+ return fromWinePath(path);
+ }
+ return QDir::cleanPath(path);
+#endif
+}
+
+QString normalizePathForWine(const QString& path)
+{
+#ifdef _WIN32
+ return QDir::toNativeSeparators(path);
+#else
+ if (isWindowsDrivePath(path)) {
+ QString p = path;
+ p.replace('/', '\\');
+ return p;
+ }
+ return toWinePath(path);
+#endif
+}
+
+#ifdef _WIN32
+QString ToString(const SYSTEMTIME& time)
+{
+ char dateBuffer[100];
+ char timeBuffer[100];
+ int size = 100;
+ GetDateFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, dateBuffer,
+ size);
+ GetTimeFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, timeBuffer,
+ size);
+ return QString::fromLocal8Bit(dateBuffer) + " " + QString::fromLocal8Bit(timeBuffer);
+}
+#endif
+
+static int naturalCompareI(const QString& a, const QString& b)
+{
+ static QCollator c = [] {
+ QCollator temp;
+ temp.setNumericMode(true);
+ temp.setCaseSensitivity(Qt::CaseInsensitive);
+ return temp;
+ }();
+
+ return c.compare(a, b);
+}
+
+int naturalCompare(const QString& a, const QString& b, Qt::CaseSensitivity cs)
+{
+ if (cs == Qt::CaseInsensitive) {
+ return naturalCompareI(a, b);
+ }
+
+ static QCollator c = [] {
+ QCollator temp;
+ temp.setNumericMode(true);
+ return temp;
+ }();
+
+ return c.compare(a, b);
+}
+
+#ifdef _WIN32
+struct CoTaskMemFreer
+{
+ void operator()(void* p) { ::CoTaskMemFree(p); }
+};
+
+template <class T>
+using COMMemPtr = std::unique_ptr<T, CoTaskMemFreer>;
+
+QString getOptionalKnownFolder(KNOWNFOLDERID id)
+{
+ COMMemPtr<wchar_t> path;
+ {
+ wchar_t* rawPath = nullptr;
+ HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath);
+ if (FAILED(res)) {
+ return {};
+ }
+ path.reset(rawPath);
+ }
+ return QString::fromWCharArray(path.get());
+}
+
+QDir getKnownFolder(KNOWNFOLDERID id, const QString& what)
+{
+ COMMemPtr<wchar_t> path;
+ {
+ wchar_t* rawPath = nullptr;
+ HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath);
+ if (FAILED(res)) {
+ log::error("failed to get known folder '{}', {}",
+ what.isEmpty() ? QUuid(id).toString() : what,
+ formatSystemMessage(res));
+ throw std::runtime_error("couldn't get known folder path");
+ }
+ path.reset(rawPath);
+ }
+ return QString::fromWCharArray(path.get());
+}
+#endif
+
+QString getDesktopDirectory()
+{
+#ifdef _WIN32
+ return getKnownFolder(FOLDERID_Desktop, "desktop").absolutePath();
+#else
+ return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
+#endif
+}
+
+QString getStartMenuDirectory()
+{
+#ifdef _WIN32
+ return getKnownFolder(FOLDERID_StartMenu, "start menu").absolutePath();
+#else
+ return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
+#endif
+}
+
+bool shellDeleteQuiet(const QString& fileName, QWidget* dialog)
+{
+ if (!QFile::remove(fileName)) {
+ return shellDelete(QStringList(fileName), false, dialog);
+ }
+ return true;
+}
+
+QString readFileText(const QString& fileName, QString* encoding, bool* hadBOM)
+{
+
+ QFile textFile(fileName);
+ if (!textFile.open(QIODevice::ReadOnly)) {
+ return QString();
+ }
+
+ QByteArray buffer = textFile.readAll();
+ return decodeTextData(buffer, encoding, hadBOM);
+}
+
+QString decodeTextData(const QByteArray& fileData, QString* encoding, bool* hadBOM)
+{
+ QStringConverter::Encoding codec = QStringConverter::Encoding::Utf8;
+ QStringEncoder encoder(codec);
+ QStringDecoder decoder(codec, QStringConverter::Flag::ConvertInitialBom);
+ QString text = decoder.decode(fileData);
+
+ // embedded nulls probably mean it was UTF-16 - they're rare/illegal in text files
+ bool hasEmbeddedNulls = false;
+ for (const auto& character : text) {
+ if (character.isNull()) {
+ hasEmbeddedNulls = true;
+ break;
+ }
+ }
+
+ // check reverse conversion
+ if (hasEmbeddedNulls || encoder.encode(text) != fileData) {
+ log::debug("conversion failed assuming local encoding");
+ auto codecSearch = QStringConverter::encodingForData(fileData);
+ if (codecSearch.has_value()) {
+ codec = codecSearch.value();
+ decoder = QStringDecoder(codec, QStringConverter::Flag::ConvertInitialBom);
+ } else {
+ decoder = QStringDecoder(hasEmbeddedNulls ? QStringConverter::Encoding::Utf16
+ : QStringConverter::Encoding::System);
+ }
+ text = decoder.decode(fileData);
+ }
+
+ if (encoding != nullptr) {
+ *encoding = QStringConverter::nameForEncoding(codec);
+ }
+
+ if (!text.isEmpty() && text.startsWith(QChar::ByteOrderMark)) {
+ text.remove(0, 1);
+
+ if (hadBOM != nullptr) {
+ *hadBOM = true;
+ }
+ } else if (hadBOM != nullptr) {
+ *hadBOM = false;
+ }
+
+ return text;
+}
+
+void removeOldFiles(const QString& path, const QString& pattern, int numToKeep,
+ QDir::SortFlags sorting)
+{
+ QFileInfoList files =
+ QDir(path).entryInfoList(QStringList(pattern), QDir::Files, sorting);
+
+ if (files.count() > numToKeep) {
+ QStringList deleteFiles;
+ for (int i = 0; i < files.count() - numToKeep; ++i) {
+ deleteFiles.append(files.at(i).absoluteFilePath());
+ }
+
+ if (!shellDelete(deleteFiles)) {
+ log::warn("failed to remove log files");
+ }
+ }
+}
+
+QIcon iconForExecutable(const QString& filePath)
+{
+ static QHash<QString, QIcon> cache;
+
+ const QFileInfo fi(filePath);
+ if (!fi.exists()) {
+ return QIcon(":/MO/gui/executable");
+ }
+
+ const QString cacheKey =
+ fi.canonicalFilePath() + "|" + QString::number(fi.size()) + "|" +
+ fi.lastModified().toString(Qt::ISODateWithMs);
+
+ if (cache.contains(cacheKey)) {
+ return cache.value(cacheKey);
+ }
+
+#ifdef _WIN32
+ QIcon icon(":/MO/gui/executable");
+ cache.insert(cacheKey, icon);
+ return icon;
+#else
+ // Try to extract icon resources from PE executables via icoutils/wrestool.
+ // If this fails, return a generic executable icon.
+ QIcon icon;
+
+ const QString wrestool = QStandardPaths::findExecutable("wrestool");
+ if (!wrestool.isEmpty()) {
+ const QString cacheRoot =
+ QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
+ "/exe_icons";
+ QDir().mkpath(cacheRoot);
+
+ const QString outDirPath = cacheRoot + "/" + QString::number(qHash(cacheKey), 16);
+ QDir outDir(outDirPath);
+ if (!outDir.exists()) {
+ QDir().mkpath(outDirPath);
+ }
+
+ auto findBestIcon = [&](const QString& dirPath) -> QIcon {
+ QString bestFile;
+ int bestArea = -1;
+
+ QDirIterator it(dirPath,
+ QStringList() << "*.png"
+ << "*.ico"
+ << "*.bmp",
+ QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ const QString path = it.next();
+ QImageReader reader(path);
+ const QSize size = reader.size();
+ const int area = size.width() * size.height();
+ if (area > bestArea) {
+ bestArea = area;
+ bestFile = path;
+ } else if (bestArea < 0) {
+ // Unknown image size (e.g. multi-size ico); keep first viable file.
+ bestFile = path;
+ }
+ }
+
+ if (!bestFile.isEmpty()) {
+ QIcon candidate(bestFile);
+ if (!candidate.pixmap(24, 24).isNull() || !candidate.pixmap(32, 32).isNull()) {
+ return candidate;
+ }
+ }
+
+ return QIcon();
+ };
+
+ // Reuse previous extraction if present.
+ icon = findBestIcon(outDirPath);
+
+ if (icon.isNull()) {
+ const QList<QStringList> tries = {
+ {"-x", "--type=group_icon", "-o", outDirPath, fi.absoluteFilePath()},
+ {"-x", "--type=14", "-o", outDirPath, fi.absoluteFilePath()},
+ {"-x", "-t14", "-o", outDirPath, fi.absoluteFilePath()},
+ };
+
+ for (const auto& args : tries) {
+ QProcess p;
+ p.start(wrestool, args);
+ if (!p.waitForFinished(4000)) {
+ p.kill();
+ continue;
+ }
+ if (p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0) {
+ icon = findBestIcon(outDirPath);
+ if (!icon.isNull()) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (icon.isNull()) {
+ icon = QIcon(":/MO/gui/executable");
+ }
+
+ cache.insert(cacheKey, icon);
+ return icon;
+#endif
+}
+
+QString getFileVersion(QString const& filepath)
+{
+ // On Linux, there is no Windows PE file version. Return empty.
+ (void)filepath;
+ return "";
+}
+
+QString getProductVersion(QString const& filepath)
+{
+ // On Linux, there is no Windows PE product version. Return empty.
+ (void)filepath;
+ return "";
+}
+
+void deleteChildWidgets(QWidget* w)
+{
+ auto* ly = w->layout();
+ if (!ly) {
+ return;
+ }
+
+ while (auto* item = ly->takeAt(0)) {
+ delete item->widget();
+ delete item;
+ }
+}
+
+void trimWString(std::wstring& s)
+{
+ s.erase(std::remove_if(s.begin(), s.end(),
+ [](wint_t ch) {
+ return std::iswspace(ch);
+ }),
+ s.end());
+}
+
+std::wstring formatMessage(DWORD id, const std::wstring& message)
+{
+ std::wstring s;
+
+ std::wostringstream oss;
+ oss << L"0x" << std::hex << id;
+
+ if (message.empty()) {
+ s = oss.str();
+ } else {
+ s += message + L" (" + oss.str() + L")";
+ }
+
+ return s;
+}
+
+std::wstring formatSystemMessage(DWORD id)
+{
+#ifdef _WIN32
+ std::wstring getMessage(DWORD id, HMODULE mod);
+ return formatMessage(id, getMessage(id, 0));
+#else
+ // On Linux, map common error codes to strings or use strerror for errno values
+ if (id == 0) {
+ return L"Success";
+ }
+ // If it looks like an errno value (small numbers), use strerror
+ if (id < 200) {
+ const char* msg = strerror(static_cast<int>(id));
+ if (msg) {
+ std::string s(msg);
+ return std::wstring(s.begin(), s.end());
+ }
+ }
+ return formatMessage(id, L"");
+#endif
+}
+
+std::wstring formatNtMessage(NTSTATUS s)
+{
+#ifdef _WIN32
+ const DWORD id = static_cast<DWORD>(s);
+ std::wstring getMessage(DWORD id, HMODULE mod);
+ return formatMessage(id, getMessage(id, ::GetModuleHandleW(L"ntdll.dll")));
+#else
+ return formatMessage(static_cast<DWORD>(s), L"NT status");
+#endif
+}
+
+QString windowsErrorString(DWORD errorCode)
+{
+ return QString::fromStdWString(formatSystemMessage(errorCode));
+}
+
+QString localizedSize(unsigned long long bytes, const QString& B, const QString& KB,
+ const QString& MB, const QString& GB, const QString& TB)
+{
+ constexpr unsigned long long OneKB = 1024ull;
+ constexpr unsigned long long OneMB = 1024ull * 1024;
+ constexpr unsigned long long OneGB = 1024ull * 1024 * 1024;
+ constexpr unsigned long long OneTB = 1024ull * 1024 * 1024 * 1024;
+
+ auto makeNum = [&](int factor) {
+ const double n = static_cast<double>(bytes) / std::pow(1024.0, factor);
+ const double truncated =
+ static_cast<double>(static_cast<unsigned long long>(n * 100)) / 100.0;
+ return QString().setNum(truncated, 'f', 2);
+ };
+
+ if (bytes < OneKB) {
+ return B.arg(bytes);
+ } else if (bytes < OneMB) {
+ return KB.arg(makeNum(1));
+ } else if (bytes < OneGB) {
+ return MB.arg(makeNum(2));
+ } else if (bytes < OneTB) {
+ return GB.arg(makeNum(3));
+ } else {
+ return TB.arg(makeNum(4));
+ }
+}
+
+QDLLEXPORT QString localizedByteSize(unsigned long long bytes)
+{
+ return localizedSize(bytes, QObject::tr("%1 B"), QObject::tr("%1 KB"),
+ QObject::tr("%1 MB"), QObject::tr("%1 GB"),
+ QObject::tr("%1 TB"));
+}
+
+QDLLEXPORT QString localizedByteSpeed(unsigned long long bps)
+{
+ return localizedSize(bps, QObject::tr("%1 B/s"), QObject::tr("%1 KB/s"),
+ QObject::tr("%1 MB/s"), QObject::tr("%1 GB/s"),
+ QObject::tr("%1 TB/s"));
+}
+
+QDLLEXPORT QString localizedTimeRemaining(unsigned int remaining)
+{
+ QString Result;
+ double interval;
+ qint64 intval;
+
+ // Hours
+ interval = 60.0 * 60.0 * 1000.0;
+ intval = (qint64)trunc((double)remaining / interval);
+ if (intval < 0)
+ intval = 0;
+ remaining -= static_cast<unsigned int>(trunc((double)intval * interval));
+ qint64 hours = intval;
+
+ // Minutes
+ interval = 60.0 * 1000.0;
+ intval = (qint64)trunc((double)remaining / interval);
+ if (intval < 0)
+ intval = 0;
+ remaining -= static_cast<unsigned int>(trunc((double)intval * interval));
+ qint64 minutes = intval;
+
+ // Seconds
+ interval = 1000.0;
+ intval = (qint64)trunc((double)remaining / interval);
+ if (intval < 0)
+ intval = 0;
+ remaining -= static_cast<unsigned int>(trunc((double)intval * interval));
+ qint64 seconds = intval;
+
+ char buffer[25];
+ memset(buffer, 0, 25);
+
+ if (hours > 0) {
+ if (hours < 10)
+ snprintf(buffer, sizeof(buffer), "0%lld", (long long)hours);
+ else
+ snprintf(buffer, sizeof(buffer), "%lld", (long long)hours);
+ Result.append(QString("%1:").arg(buffer));
+ }
+
+ if (minutes > 0 || hours > 0) {
+ if (minutes < 10 && hours > 0)
+ snprintf(buffer, sizeof(buffer), "0%lld", (long long)minutes);
+ else
+ snprintf(buffer, sizeof(buffer), "%lld", (long long)minutes);
+ Result.append(QString("%1:").arg(buffer));
+ }
+
+ if (seconds < 10 && (minutes > 0 || hours > 0))
+ snprintf(buffer, sizeof(buffer), "0%lld", (long long)seconds);
+ else
+ snprintf(buffer, sizeof(buffer), "%lld", (long long)seconds);
+ Result.append(QString("%1").arg(buffer));
+
+ if (hours > 0)
+ //: Time remaining hours
+ Result.append(QApplication::translate("uibase", "h"));
+ else if (minutes > 0)
+ //: Time remaining minutes
+ Result.append(QApplication::translate("uibase", "m"));
+ else
+ //: Time remaining seconds
+ Result.append(QApplication::translate("uibase", "s"));
+
+ return Result;
+}
+
+QDLLEXPORT void localizedByteSizeTests()
+{
+ auto f = [](unsigned long long n) {
+ return localizedByteSize(n).toStdString();
+ };
+
+#define CHECK_EQ(a, b) \
+ if ((a) != (b)) { \
+ std::cerr << "failed: " << a << " == " << b << "\n"; \
+ }
+
+ CHECK_EQ(f(0), "0 B");
+ CHECK_EQ(f(1), "1 B");
+ CHECK_EQ(f(999), "999 B");
+ CHECK_EQ(f(1000), "1000 B");
+ CHECK_EQ(f(1023), "1023 B");
+
+ CHECK_EQ(f(1024), "1.00 KB");
+ CHECK_EQ(f(2047), "1.99 KB");
+ CHECK_EQ(f(2048), "2.00 KB");
+ CHECK_EQ(f(1048575), "1023.99 KB");
+
+ CHECK_EQ(f(1048576), "1.00 MB");
+ CHECK_EQ(f(1073741823), "1023.99 MB");
+
+ CHECK_EQ(f(1073741824), "1.00 GB");
+ CHECK_EQ(f(1099511627775), "1023.99 GB");
+
+ CHECK_EQ(f(1099511627776), "1.00 TB");
+ CHECK_EQ(f(2759774185818), "2.51 TB");
+
+#undef CHECK_EQ
+}
+
+TimeThis::TimeThis(const QString& what) : m_running(false)
+{
+ start(what);
+}
+
+TimeThis::~TimeThis()
+{
+ stop();
+}
+
+void TimeThis::start(const QString& what)
+{
+ stop();
+
+ m_what = what;
+ m_start = Clock::now();
+ m_running = true;
+}
+
+void TimeThis::stop()
+{
+ using namespace std::chrono;
+
+ if (!m_running) {
+ return;
+ }
+
+ const auto end = Clock::now();
+ const auto d = duration_cast<milliseconds>(end - m_start).count();
+
+ if (m_what.isEmpty()) {
+ log::debug("timing: {} ms", d);
+ } else {
+ log::debug("timing: {} {} ms", m_what, d);
+ }
+
+ m_running = false;
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/version.rc b/libs/uibase/src/version.rc
new file mode 100644
index 0000000..fdea5f9
--- /dev/null
+++ b/libs/uibase/src/version.rc
@@ -0,0 +1,37 @@
+#include "Winver.h"
+
+// If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number.
+// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser
+// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha
+#define VER_FILEVERSION 2.5.3
+#define VER_FILEVERSION_STR "2.5.3\0"
+
+VS_VERSION_INFO VERSIONINFO
+FILEVERSION VER_FILEVERSION
+PRODUCTVERSION VER_FILEVERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+FILEFLAGS (0)
+FILEOS VOS__WINDOWS32
+FILETYPE VFT_APP
+FILESUBTYPE (0)
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904B0"
+ BEGIN
+ VALUE "FileVersion", VER_FILEVERSION_STR
+ VALUE "CompanyName", "Mod Organizer 2 Team\0"
+ VALUE "FileDescription", "MO2 Base UI plugin API library\0"
+ VALUE "OriginalFilename", "uibase.dll\0"
+ VALUE "InternalName", "uibase\0"
+ VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2026 Mod Organizer 2 contributors\0"
+ VALUE "ProductName", "Mod Organizer 2 UI Base\0"
+ VALUE "ProductVersion", VER_FILEVERSION_STR
+ END
+ END
+
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0409, 1200
+ END
+END
diff --git a/libs/uibase/src/versioninfo.cpp b/libs/uibase/src/versioninfo.cpp
new file mode 100644
index 0000000..6dec18e
--- /dev/null
+++ b/libs/uibase/src/versioninfo.cpp
@@ -0,0 +1,432 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/versioninfo.h>
+#include <QDate>
+#include <QLocale>
+#include <QRegularExpression>
+#include <QVersionNumber>
+
+namespace
+{
+const QRegularExpression VERSION_REGEX("^(\\d+)(\\.(\\d+))?(\\.(\\d+))?(\\.(\\d+))?");
+}
+
+namespace MOBase
+{
+
+VersionInfo::VersionInfo()
+ : m_Scheme(SCHEME_REGULAR), m_Valid(false), m_ReleaseType(RELEASE_FINAL),
+ m_Major(0), m_Minor(0), m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0),
+ m_Rest()
+{}
+
+VersionInfo::VersionInfo(int major, int minor, int subminor, int subsubminor,
+ ReleaseType releaseType)
+ : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType),
+ m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(subsubminor),
+ m_DecimalPositions(0), m_Rest()
+{}
+
+VersionInfo::VersionInfo(int major, int minor, int subminor, ReleaseType releaseType)
+ : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType),
+ m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(0),
+ m_DecimalPositions(0), m_Rest()
+{}
+
+VersionInfo::VersionInfo(const QString& versionString, VersionScheme scheme)
+ : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0),
+ m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest()
+{
+ parse(versionString, scheme);
+}
+
+VersionInfo::VersionInfo(const QString& versionString,
+ VersionInfo::VersionScheme scheme, bool manualInput)
+ : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0),
+ m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest()
+{
+ parse(versionString, scheme, manualInput);
+}
+
+void VersionInfo::clear()
+{
+ m_Scheme = SCHEME_REGULAR;
+ m_Valid = false;
+ m_ReleaseType = RELEASE_FINAL;
+ m_Major = m_Minor = m_SubMinor = m_SubSubMinor = m_DecimalPositions = 0;
+ m_Rest.clear();
+}
+
+QString VersionInfo::canonicalString() const
+{
+ if (!isValid()) {
+ return QString();
+ }
+
+ QString result;
+ if (m_Scheme == SCHEME_REGULAR) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DECIMALMARK) {
+ result = QString("f%1.%2").arg(m_Major).arg(
+ QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0'));
+ } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) {
+ result = QString("n%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DATE) {
+ // year.month.day was stored in the version fields
+ result = QString("d%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ }
+ switch (m_ReleaseType) {
+ case RELEASE_PREALPHA: {
+ result.append(" pre-alpha");
+ } break;
+ case RELEASE_ALPHA: {
+ result.append("a");
+ } break;
+ case RELEASE_BETA: {
+ result.append("b");
+ } break;
+ case RELEASE_CANDIDATE: {
+ result.append("rc");
+ } break;
+ case RELEASE_FINAL: // fall-through
+ default: {
+ // nop
+ } break;
+ }
+
+ if (!m_Rest.isEmpty()) {
+ result.append(QString("%1").arg(m_Rest));
+ }
+
+ return result;
+}
+
+QString VersionInfo::displayString(int forcedVersionSegments) const
+{
+ if (!isValid()) {
+ return QString();
+ }
+
+ QString result;
+ if (m_Scheme == SCHEME_REGULAR) {
+ if (forcedVersionSegments >= 4 || m_SubSubMinor != 0) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (forcedVersionSegments == 3 || m_SubMinor != 0) {
+ result = QString("%1.%2.%3").arg(m_Major).arg(m_Minor).arg(m_SubMinor);
+ } else {
+ result = QString("%1.%2").arg(m_Major).arg(m_Minor);
+ }
+ } else if (m_Scheme == SCHEME_DECIMALMARK) {
+ result = QString("%1.%2").arg(m_Major).arg(
+ QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0'));
+ } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) {
+ result = QString("%1.%2.%3.%4")
+ .arg(m_Major)
+ .arg(m_Minor)
+ .arg(m_SubMinor)
+ .arg(m_SubSubMinor);
+ } else if (m_Scheme == SCHEME_DATE) {
+ // year.month.day was stored in the version fields
+ const auto year = m_Major;
+ const auto month = m_Minor;
+ const auto day = m_SubMinor;
+
+ return QLocale::system().toString(QDate(year, month, day),
+ QLocale::FormatType::ShortFormat);
+ }
+ switch (m_ReleaseType) {
+ case RELEASE_PREALPHA: {
+ result.append(" pre-alpha");
+ } break;
+ case RELEASE_ALPHA: {
+ result.append("alpha");
+ } break;
+ case RELEASE_BETA: {
+ result.append("beta");
+ } break;
+ case RELEASE_CANDIDATE: {
+ result.append("rc");
+ } break;
+ case RELEASE_FINAL: // fall-through
+ default: {
+ // nop
+ } break;
+ }
+
+ if (!m_Rest.isEmpty()) {
+ result.append(QString("%1").arg(m_Rest));
+ }
+
+ return result;
+}
+
+QVersionNumber VersionInfo::asQVersionNumber() const
+{
+ return QVersionNumber::fromString(displayString()).normalized();
+}
+
+QString VersionInfo::parseReleaseType(QString versionString)
+{
+ // release types are often followed by a number (i.e. "beta4"). This needs to be
+ // extracted now, otherwise the outer parser will think it's the subminor version and
+ // then 1.0.0rc1 would be interpreted as newer than 1.0.0
+
+ static std::map<QString, ReleaseType> typeStrings = {{"prealpha", RELEASE_PREALPHA},
+ {"alpha", RELEASE_ALPHA},
+ {"beta", RELEASE_BETA},
+ {"rc", RELEASE_CANDIDATE}};
+
+ m_ReleaseType = RELEASE_FINAL;
+
+ auto typeIter = typeStrings.begin();
+ int offset = -1;
+
+ for (; (typeIter != typeStrings.end()) && (offset == -1); ++typeIter) {
+ offset = (int)versionString.indexOf(typeIter->first, Qt::CaseInsensitive);
+ if (offset != -1) {
+ m_ReleaseType = typeIter->second;
+ break;
+ }
+ }
+
+ int length = 0;
+
+ if (typeIter != typeStrings.end()) {
+ length = (int)typeIter->first.length();
+ }
+
+ if (m_Scheme == SCHEME_REGULAR) {
+ // also interpret the a/b letters, but only if they follow immediately on the
+ // version number, otherwise the margin for error is too big
+ if ((offset == -1) && (versionString.length() > 0)) {
+ if (versionString.at(0) == 'a') {
+ m_ReleaseType = RELEASE_ALPHA;
+ offset = 0;
+ length = 1;
+ } else if (versionString.at(0) == 'b') {
+ m_ReleaseType = RELEASE_BETA;
+ offset = 0;
+ length = 1;
+ }
+ }
+ }
+
+ if (offset != -1) {
+ versionString.remove(offset, length);
+ }
+ return versionString.trimmed();
+}
+
+void VersionInfo::parse(const QString& versionString, VersionScheme scheme,
+ bool manualInput)
+{
+ m_Valid = false;
+ m_Scheme = scheme == SCHEME_LITERAL ? SCHEME_REGULAR
+ : scheme != SCHEME_DISCOVER ? scheme
+ : SCHEME_REGULAR;
+ m_ReleaseType = RELEASE_FINAL;
+ m_Major = m_Minor = m_SubMinor = m_SubSubMinor = 0;
+ m_Rest.clear();
+ if (versionString.length() == 0) {
+ return;
+ }
+
+ if (QString::compare(versionString, "final", Qt::CaseInsensitive) == 0) {
+ m_Major = 1;
+ m_Valid = true;
+ return;
+ }
+
+ QString temp = versionString;
+ // first, determine the versioning scheme if there is a hint
+ VersionScheme newScheme = m_Scheme;
+ if (!manualInput) {
+ if (temp.startsWith('f')) {
+ newScheme = SCHEME_DECIMALMARK;
+ temp.remove(0, 1);
+ } else if (temp.startsWith('n')) {
+ newScheme = SCHEME_NUMBERSANDLETTERS;
+ temp.remove(0, 1);
+ } else if (temp.startsWith('d')) {
+ newScheme = SCHEME_DATE;
+ temp.remove(0, 1);
+ }
+ }
+
+ if (scheme == SCHEME_DISCOVER) {
+ m_Scheme = newScheme;
+ }
+
+ if (temp.startsWith('v', Qt::CaseInsensitive)) {
+ // v is often prepended to versions
+ temp.remove(0, 1);
+ }
+
+ auto match = VERSION_REGEX.match(temp);
+ if (match.hasMatch()) {
+ m_Major = match.captured(1).toInt();
+ m_Minor = match.captured(3).toInt();
+ QString subMinor = match.captured(5);
+ QString subSubMinor = match.captured(7);
+ if (!subMinor.isEmpty() && (m_Scheme == SCHEME_DECIMALMARK)) {
+ // nooooope, if there are two dots it can't be a decimal mark
+ m_Scheme = SCHEME_REGULAR;
+ }
+ if (m_Scheme != SCHEME_DECIMALMARK) {
+ m_SubMinor = subMinor.toInt();
+ m_SubSubMinor = subSubMinor.toInt();
+ }
+ if (subMinor.isEmpty() && (match.captured(3).size() > 1) &&
+ match.captured(3).startsWith('0')) {
+ // this indicates a decimal scheme
+ m_Scheme = SCHEME_DECIMALMARK;
+ m_DecimalPositions = (int)match.captured(3).size();
+ }
+ temp.remove(VERSION_REGEX);
+ } else {
+ m_Scheme = SCHEME_LITERAL;
+ }
+
+ if (m_Scheme == SCHEME_REGULAR) {
+ temp = parseReleaseType(temp);
+ }
+
+ if ((m_Scheme == SCHEME_DATE) && (m_Major < 1900)) {
+ m_Scheme = SCHEME_REGULAR;
+ }
+ m_Rest = temp.trimmed();
+ m_Valid = true;
+}
+
+QDLLEXPORT bool operator<(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ if (!LHS.isValid() && RHS.isValid())
+ return true;
+ if (!RHS.isValid() && LHS.isValid())
+ return false;
+
+ // date-releases are lower than regular versions
+ if ((LHS.m_Scheme == VersionInfo::SCHEME_DATE) &&
+ (RHS.m_Scheme != VersionInfo::SCHEME_DATE)) {
+ return true;
+ } else if ((LHS.m_Scheme != VersionInfo::SCHEME_DATE) &&
+ (RHS.m_Scheme == VersionInfo::SCHEME_DATE)) {
+ return false;
+ } else if ((LHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK) ||
+ (RHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK)) {
+ // use decimal versioning if either version is a decimal. The parser interprets
+ // versions as regular if in doubt so if the scheme is "decimal" it is definitively
+ // a decimal version number whereas SCHEME_REGULAR means "probably regular"
+
+ float leftVal = QString("%1.%2")
+ .arg(LHS.m_Major)
+ .arg(QString("%1")
+ .arg(LHS.m_Minor)
+ .rightJustified(LHS.m_DecimalPositions, '0'))
+ .toFloat();
+ float rightVal = QString("%1.%2")
+ .arg(RHS.m_Major)
+ .arg(QString("%1")
+ .arg(RHS.m_Minor)
+ .rightJustified(RHS.m_DecimalPositions, '0'))
+ .toFloat();
+ if (fabs(leftVal - rightVal) > 0.001f) {
+ return leftVal < rightVal;
+ }
+ } else {
+ // if in doubt, use the sane choice. regular and numbers+letters can be treated the
+ // same way
+ if (LHS.m_Major != RHS.m_Major)
+ return LHS.m_Major < RHS.m_Major;
+ if (LHS.m_Minor != RHS.m_Minor)
+ return LHS.m_Minor < RHS.m_Minor;
+ if (LHS.m_SubMinor != RHS.m_SubMinor)
+ return LHS.m_SubMinor < RHS.m_SubMinor;
+ if (LHS.m_SubSubMinor != RHS.m_SubSubMinor)
+ return LHS.m_SubSubMinor < RHS.m_SubSubMinor;
+ }
+
+ // subminor, release-type and rest are treated the same for all versioning schemes,
+ // but on parsing they may still differ, i.e. a b-suffix is only interpreted to mean
+ // "beta" in the regular scheme
+ if (LHS.m_ReleaseType != RHS.m_ReleaseType)
+ return LHS.m_ReleaseType < RHS.m_ReleaseType;
+
+ // if the rest contains only integers, compare them numerically
+ bool LHS_ok, RHS_ok;
+ int LHS_int, RHS_int;
+ LHS_int = LHS.m_Rest.toInt(&LHS_ok);
+ RHS_int = RHS.m_Rest.toInt(&RHS_ok);
+ if (LHS_ok && RHS_ok) {
+ return LHS_int < RHS_int;
+ }
+
+ // give up and compare lexically
+ return LHS.m_Rest < RHS.m_Rest;
+}
+
+QDLLEXPORT bool operator>(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return !(LHS <= RHS);
+}
+
+QDLLEXPORT bool operator<=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ // TODO not exactly optimized...
+ if (LHS < RHS) {
+ return true;
+ } else {
+ return !(RHS < LHS);
+ }
+}
+
+QDLLEXPORT bool operator>=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return RHS <= LHS;
+}
+
+QDLLEXPORT bool operator!=(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return (LHS < RHS) || (RHS < LHS);
+}
+
+QDLLEXPORT bool operator==(const VersionInfo& LHS, const VersionInfo& RHS)
+{
+ return !(LHS < RHS) && !(RHS < LHS);
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/versioning.cpp b/libs/uibase/src/versioning.cpp
new file mode 100644
index 0000000..8178841
--- /dev/null
+++ b/libs/uibase/src/versioning.cpp
@@ -0,0 +1,278 @@
+#include <uibase/versioning.h>
+
+#include <QRegularExpression>
+#include <format>
+#include <unordered_map>
+
+#include <uibase/formatters.h>
+
+// official semver regex
+static const QRegularExpression s_SemVerStrictRegEx{
+ R"(^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"};
+
+// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX
+static const QRegularExpression s_SemVerMO2RegEx{
+ R"(^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:\.(?P<subpatch>0|[1-9]\d*))?(?:(?P<type>dev|a|alpha|b|beta|rc)(?P<prerelease>0|[1-9](?:[.0-9])*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"};
+
+// match from value to release type
+static const std::unordered_map<QString, MOBase::Version::ReleaseType>
+ s_StringToRelease{{"dev", MOBase::Version::Development},
+ {"alpha", MOBase::Version::Alpha},
+ {"a", MOBase::Version::Alpha},
+ {"beta", MOBase::Version::Beta},
+ {"b", MOBase::Version::Beta},
+ {"rc", MOBase::Version::ReleaseCandidate}};
+
+namespace MOBase
+{
+
+namespace
+{
+
+ Version parseVersionSemVer(QString const& value)
+ {
+ const auto match = s_SemVerStrictRegEx.match(value);
+
+ if (!match.hasMatch()) {
+ throw InvalidVersionException(
+ QString::fromStdString(std::format("invalid version string: '{}'", value)));
+ }
+
+ const auto major = match.captured("major").toInt();
+ const auto minor = match.captured("minor").toInt();
+ const auto patch = match.captured("patch").toInt();
+
+ std::vector<std::variant<int, Version::ReleaseType>> prereleases;
+ for (auto& part : match.captured("prerelease")
+ .split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) {
+ // try to extract an int
+ bool ok = true;
+ const auto intValue = part.toInt(&ok);
+ if (ok) {
+ prereleases.push_back(intValue);
+ continue;
+ }
+
+ // check if we have a valid prerelease type
+ const auto it = s_StringToRelease.find(part.toLower());
+ if (it == s_StringToRelease.end()) {
+ throw InvalidVersionException(
+ QString::fromStdString(std::format("invalid prerelease type: '{}'", part)));
+ }
+
+ prereleases.push_back(it->second);
+ }
+
+ const auto buildMetadata = match.captured("buildmetadata").trimmed();
+
+ return Version(major, minor, patch, 0, prereleases, buildMetadata);
+ }
+
+ Version parseVersionMO2(QString const& value)
+ {
+ const auto match = s_SemVerMO2RegEx.match(value);
+
+ if (!match.hasMatch()) {
+ throw InvalidVersionException(
+ QString::fromStdString(std::format("invalid version string: '{}'", value)));
+ }
+
+ const auto major = match.captured("major").toInt();
+ const auto minor = match.captured("minor").toInt();
+ const auto patch = match.captured("patch").toInt();
+
+ const auto subpatch = match.captured("subpatch").toInt();
+
+ // unlike semver, the regex will only match valid values
+ std::vector<std::variant<int, Version::ReleaseType>> prereleases;
+ if (match.hasCaptured("type")) {
+ prereleases.push_back(s_StringToRelease.at(match.captured("type")));
+
+ // for version with decimal point, e.g., 2.4.1rc1.1, we split the components into
+ // pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc,
+ // 1, 1}
+ //
+ for (const auto& preVersion :
+ match.captured("prerelease").split(".", Qt::SkipEmptyParts)) {
+ prereleases.push_back(preVersion.toInt());
+ }
+ }
+
+ const auto buildMetadata = match.captured("buildmetadata").trimmed();
+
+ return Version(major, minor, patch, subpatch, prereleases, buildMetadata);
+ }
+
+} // namespace
+
+Version Version::parse(QString const& value, ParseMode mode)
+{
+ return mode == ParseMode::SemVer ? parseVersionSemVer(value) : parseVersionMO2(value);
+}
+
+// constructors
+
+Version::Version(int major, int minor, int patch, QString metadata)
+ : Version(major, minor, patch, 0, std::move(metadata))
+{}
+Version::Version(int major, int minor, int patch, int subpatch, QString metadata)
+ : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch},
+ m_PreReleases{}, m_BuildMetadata{std::move(metadata)}
+{}
+
+Version::Version(int major, int minor, int patch, ReleaseType type, QString metadata)
+ : Version(major, minor, patch, 0, type, std::move(metadata))
+{}
+Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type,
+ QString metadata)
+ : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch},
+ m_PreReleases{type}, m_BuildMetadata{std::move(metadata)}
+{}
+
+Version::Version(int major, int minor, int patch, ReleaseType type, int prerelease,
+ QString metadata)
+ : Version(major, minor, patch, 0, type, prerelease, std::move(metadata))
+{}
+Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type,
+ int prerelease, QString metadata)
+ : Version(major, minor, patch, subpatch, {type, prerelease}, std::move(metadata))
+{}
+
+Version::Version(int major, int minor, int patch, int subpatch,
+ std::vector<std::variant<int, ReleaseType>> prereleases,
+ QString metadata)
+ : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch},
+ m_PreReleases{std::move(prereleases)}, m_BuildMetadata{std::move(metadata)}
+{}
+
+// string
+
+QString Version::string(const FormatModes& modes) const
+{
+ const bool noSeparator = modes.testFlag(FormatMode::NoSeparator);
+ const bool shortAlphaBeta = modes.testFlag(FormatMode::ShortAlphaBeta);
+ auto value = std::format("{}.{}.{}", m_Major, m_Minor, m_Patch);
+
+ if (m_SubPatch || modes.testFlag(FormatMode::ForceSubPatch)) {
+ value += std::format(".{}", m_SubPatch);
+ }
+
+ if (!m_PreReleases.empty()) {
+ if (!noSeparator) {
+ value += "-";
+ }
+ for (std::size_t i = 0; i < m_PreReleases.size(); ++i) {
+ value += std::visit(
+ [shortAlphaBeta](auto const& pre) -> std::string {
+ if constexpr (std::is_same_v<decltype(pre), ReleaseType const&>) {
+ switch (pre) {
+ case Development:
+ return "dev";
+ case Alpha:
+ return shortAlphaBeta ? "a" : "alpha";
+ case Beta:
+ return shortAlphaBeta ? "b" : "beta";
+ case ReleaseCandidate:
+ return "rc";
+ }
+ return "";
+ } else {
+ return std::to_string(pre);
+ }
+ },
+ m_PreReleases[i]);
+ if (!noSeparator && i < m_PreReleases.size() - 1) {
+ value += ".";
+ }
+ }
+ }
+
+ if (!modes.testFlag(FormatMode::NoMetadata) && !m_BuildMetadata.isEmpty()) {
+ value += "+" + m_BuildMetadata.toStdString();
+ }
+
+ return QString::fromStdString(value);
+}
+
+namespace
+{
+ // consume the given iterator until the given end iterator or until a non-zero value
+ // is found
+ //
+ template <class It>
+ It consumePreReleaseZeros(It it, It end)
+ {
+ for (; it != end; ++it) {
+ if (!std::holds_alternative<int>(*it) != 0 || std::get<int>(*it) != 0) {
+ break;
+ }
+ }
+ return it;
+ };
+} // namespace
+
+std::strong_ordering operator<=>(const Version& lhs, const Version& rhs)
+{
+ auto mmp_cmp =
+ std::forward_as_tuple(lhs.major(), lhs.minor(), lhs.patch(), lhs.subpatch()) <=>
+ std::forward_as_tuple(rhs.major(), rhs.minor(), rhs.patch(), rhs.subpatch());
+
+ // major.minor.patch have precedence over everything else
+ if (mmp_cmp != std::strong_ordering::equal) {
+ return mmp_cmp;
+ }
+
+ // handle cases were one is a pre-release and not the other - the pre-release is
+ // "less" than the release
+ if (lhs.isPreRelease() && !rhs.isPreRelease()) {
+ return std::strong_ordering::less;
+ }
+
+ if (!lhs.isPreRelease() && rhs.isPreRelease()) {
+ return std::strong_ordering::greater;
+ }
+
+ // compare pre-release fields
+ auto lhsIt = lhs.preReleases().begin(), rhsIt = rhs.preReleases().begin();
+ for (; lhsIt != lhs.preReleases().end() && rhsIt != rhs.preReleases().end();
+ ++lhsIt, ++rhsIt) {
+
+ const auto &lhsPre = *lhsIt, rhsPre = *rhsIt;
+
+ // if one is alpha/beta/etc. and the other is numeric, the alpha/beta/etc. is lower
+ // than the numeric one, which matches the index
+ auto pre_cmp = lhsPre.index() <=> rhsPre.index();
+ if (pre_cmp != std::strong_ordering::equal) {
+ return pre_cmp;
+ }
+
+ // compare the actual values
+ pre_cmp = lhsPre <=> rhsPre;
+ if (pre_cmp != std::strong_ordering::equal) {
+ return pre_cmp;
+ }
+ }
+
+ // the code below does not follow semver 100% (I think) - basically, this makes stuff
+ // like 2.4.1rc1.0 equals to 2.4.1rc1, which according to semver is probably not right
+ // but is probably best for us
+ //
+
+ // if we land here, we have consumed one of the pre-release, we skip all the 0 in the
+ // remaining one
+ lhsIt = consumePreReleaseZeros(lhsIt, lhs.preReleases().end());
+ rhsIt = consumePreReleaseZeros(rhsIt, rhs.preReleases().end());
+
+ const auto lhsConsumed = lhsIt == lhs.preReleases().end(),
+ rhsConsumed = rhsIt == rhs.preReleases().end();
+
+ if (lhsConsumed && rhsConsumed) {
+ return std::strong_ordering::equal;
+ } else if (!lhsConsumed) {
+ return std::strong_ordering::greater;
+ } else {
+ return std::strong_ordering::less;
+ }
+}
+
+} // namespace MOBase
diff --git a/libs/uibase/src/widgetutility.cpp b/libs/uibase/src/widgetutility.cpp
new file mode 100644
index 0000000..e3a4b06
--- /dev/null
+++ b/libs/uibase/src/widgetutility.cpp
@@ -0,0 +1,59 @@
+#include <uibase/widgetutility.h>
+#include <uibase/eventfilter.h>
+#include <QCheckBox>
+#include <QHeaderView>
+#include <QMenu>
+#include <QWidgetAction>
+
+namespace MOBase
+{
+
+void onHeaderContextMenu(QTreeView* view, const QPoint& pos)
+{
+ auto* header = view->header();
+
+ QMenu menu;
+
+ // display a list of all headers as checkboxes
+ QAbstractItemModel* model = header->model();
+
+ for (int i = 1; i < model->columnCount(); ++i) {
+ const QString columnName = model->headerData(i, Qt::Horizontal).toString();
+
+ auto* checkBox = new QCheckBox(&menu);
+ checkBox->setText(columnName);
+ checkBox->setChecked(!header->isSectionHidden(i));
+
+ // Enable the checkbox if 1) the section is visible, or 2) the
+ // EnabledColumnRole is not found, or 3) the value for the role is true.
+ auto display = model->headerData(i, Qt::Horizontal, EnabledColumnRole);
+ checkBox->setEnabled(!header->isSectionHidden(i) || !display.isValid() ||
+ display.toBool());
+
+ auto* checkableAction = new QWidgetAction(&menu);
+ checkableAction->setDefaultWidget(checkBox);
+
+ QObject::connect(checkBox, &QCheckBox::clicked, [=] {
+ header->setSectionHidden(i, !checkBox->isChecked());
+ });
+
+ menu.addAction(checkableAction);
+ }
+
+ menu.exec(header->viewport()->mapToGlobal(pos));
+}
+
+void setCustomizableColumns(QTreeView* view)
+{
+ auto* header = view->header();
+
+ header->setSectionsMovable(true);
+ header->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ QObject::connect(header, &QWidget::customContextMenuRequested, view,
+ [view](auto&& pos) {
+ onHeaderContextMenu(view, pos);
+ });
+}
+
+} // namespace MOBase