diff options
Diffstat (limited to 'libs/uibase/src')
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>&Find Next</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="closeBtn"> + <property name="toolTip"> + <string>Close</string> + </property> + <property name="whatsThis"> + <string>Close</string> + </property> + <property name="text"> + <string>Close</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/libs/uibase/src/guessedvalue.cpp b/libs/uibase/src/guessedvalue.cpp new file mode 100644 index 0000000..c8393af --- /dev/null +++ b/libs/uibase/src/guessedvalue.cpp @@ -0,0 +1 @@ +#include <uibase/guessedvalue.h> diff --git a/libs/uibase/src/ifiletree.cpp b/libs/uibase/src/ifiletree.cpp new file mode 100644 index 0000000..b582d18 --- /dev/null +++ b/libs/uibase/src/ifiletree.cpp @@ -0,0 +1,1029 @@ +#include <uibase/ifiletree.h> + +#include <algorithm> +#include <ranges> +#include <span> +#include <stack> + +#include <QRegularExpression> + +// FileTreeEntry: +namespace MOBase +{ +FileTreeEntry::FileTreeEntry(std::shared_ptr<const IFileTree> parent, QString name) + : m_Parent(parent), m_Name(name) +{} + +QString FileTreeEntry::suffix() const +{ + const qsizetype idx = m_Name.lastIndexOf("."); + return (isDir() || idx == -1) ? "" : m_Name.mid(idx + 1); +} + +bool FileTreeEntry::hasSuffix(QString suffix) const +{ + return this->suffix().compare(suffix, FileNameComparator::CaseSensitivity) == 0; +} + +bool FileTreeEntry::hasSuffix(QStringList suffixes) const +{ + return suffixes.contains(suffix(), FileNameComparator::CaseSensitivity); +} + +QString FileTreeEntry::pathFrom(std::shared_ptr<const IFileTree> tree, + QString sep) const +{ + + // We will construct the path from right to left: + QString path = name(); + + auto p = parent(); + while (p != nullptr && p != tree) { + // We need to check the parent, otherwize we are going to prepend the name + // and a / for the base, which we do not want. + if (p->parent() != nullptr) { + path = p->name() + sep + path; + } + p = p->parent(); + } + + return p == tree ? path : QString(); +} + +bool FileTreeEntry::detach() +{ + auto p = parent(); + if (p == nullptr) { + return false; + } + return p->erase(shared_from_this()) != p->end(); +} + +bool FileTreeEntry::moveTo(std::shared_ptr<IFileTree> tree) +{ + return tree->insert(shared_from_this()) != tree->end(); +} + +std::shared_ptr<FileTreeEntry> FileTreeEntry::clone() const +{ + return createFileEntry(nullptr, name()); +} + +std::shared_ptr<FileTreeEntry> +FileTreeEntry::createFileEntry(std::shared_ptr<const IFileTree> parent, QString name) +{ + return std::shared_ptr<FileTreeEntry>(new FileTreeEntry(parent, name)); +} +} // namespace MOBase + +// IFileTree: +namespace MOBase +{ + +/** + * Comparator for file entries. + */ +struct FileEntryComparator +{ + bool operator()(std::shared_ptr<FileTreeEntry> const& a, + std::shared_ptr<FileTreeEntry> const& b) const + { + if (a->isDir() && !b->isDir()) { + return true; + } else if (!a->isDir() && b->isDir()) { + return false; + } else { + return FileNameComparator::compare(a->name(), b->name()) < 0; + } + } +}; + +/** + * @brief Comparator that can be used to find entry matching the given name and + * file type. + */ +struct MatchEntryComparator +{ + + MatchEntryComparator(QString const& name, FileTreeEntry::FileTypes matchTypes) + : m_Name(name), m_MatchTypes(matchTypes) + {} + + bool operator()(const std::shared_ptr<const FileTreeEntry>& fileEntry) const + { + return m_MatchTypes.testFlag(fileEntry->fileType()) && + fileEntry->compare(m_Name) == 0; + } + + bool operator()(const std::shared_ptr<FileTreeEntry>& fileEntry) const + { + return m_MatchTypes.testFlag(fileEntry->fileType()) && + fileEntry->compare(m_Name) == 0; + } + +private: + QString const& m_Name; + FileTreeEntry::FileTypes m_MatchTypes; +}; + +/** + * + */ +bool IFileTree::exists(QString path, FileTypes type) const +{ + return find(path, type) != nullptr; +} + +/** + * + */ +std::shared_ptr<FileTreeEntry> IFileTree::find(QString path, FileTypes type) +{ + return fetchEntry(splitPath(path), type); +} +std::shared_ptr<const FileTreeEntry> IFileTree::find(QString path, FileTypes type) const +{ + return fetchEntry(splitPath(path), type); +} + +/** + * + */ +void IFileTree::walk( + std::function<WalkReturn(QString const&, std::shared_ptr<const FileTreeEntry>)> + callback, + QString sep) const +{ + + std::stack<std::pair<QString, std::shared_ptr<const FileTreeEntry>>> stack; + + // We start by pushing all the entries in this tree, this avoid having to do extra + // check later for avoid leading separator: + for (auto rit = rbegin(); rit != rend(); ++rit) { + stack.push({"", *rit}); + } + + while (!stack.empty()) { + auto [path, entry] = stack.top(); + stack.pop(); + + auto res = callback(path, entry); + if (res == WalkReturn::STOP) { + break; + } + if (entry->isDir() && res != WalkReturn::SKIP) { + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + stack.push({path + tree->name() + sep, *rit}); + } + } + } +} + +/** + * + */ +std::shared_ptr<FileTreeEntry> IFileTree::addFile(QString path, bool replaceIfExists) +{ + QStringList parts = splitPath(path); + + // Check if the file already exists: + auto existingEntry = fetchEntry(parts, IFileTree::FILE_OR_DIRECTORY); + if (!replaceIfExists && existingEntry != nullptr) { + return nullptr; + } + + // Find or create the tree: + IFileTree* tree; + + if (parts.size() > 1) { + + // Create the tree: + std::shared_ptr<FileTreeEntry> treeEntry = + createTree(parts.begin(), parts.end() - 1); + + // Early fail if the tree was not created: + if (treeEntry == nullptr) { + return nullptr; + } + + tree = treeEntry->astree().get(); + } else { + tree = this; + } + + std::shared_ptr<FileTreeEntry> entry = + tree->makeFile(tree->astree(), parts[parts.size() - 1]); + + // If makeFile returns a null pointer, it means we cannot create file: + if (entry == nullptr) { + return nullptr; + } + + // Remove the existing files if there was one: + if (existingEntry) { + existingEntry->detach(); + } + + // Insert in the tree: + tree->entries().insert( + std::upper_bound(tree->begin(), tree->end(), entry, FileEntryComparator{}), + entry); + + return entry; +} + +/** + * + */ +std::shared_ptr<IFileTree> IFileTree::addDirectory(QString path) +{ + QStringList parts = splitPath(path); + return createTree(parts.begin(), parts.end()); +} + +/** + * + */ +IFileTree::iterator IFileTree::insert(std::shared_ptr<FileTreeEntry> entry, + InsertPolicy insertPolicy) +{ + + // Check that this is not the current tree or a parent tree: + if (entry->isDir()) { + std::shared_ptr<IFileTree> tmp = astree(); + while (tmp != nullptr) { + if (tmp == entry->astree()) { + return end(); + } + tmp = tmp->parent(); + } + } + + // Check if there exists an entry with the same name: + auto existingIt = std::find_if( + begin(), end(), MatchEntryComparator{entry->name(), FILE_OR_DIRECTORY}); + + // Already in the tree? + if (existingIt != end() && *existingIt == entry) { + return existingIt; + } + + auto insertionIt = end(); + + if (existingIt != end()) { + if (insertPolicy == InsertPolicy::FAIL_IF_EXISTS) { + return end(); + } + + // We replace if the policy is REPLACE or if the new and old entry are + // both files: + if (insertPolicy == InsertPolicy::REPLACE || + ((*existingIt)->isFile() && entry->isFile())) { + if (beforeReplace(this, existingIt->get(), entry.get())) { + // Detach the old entry from its parent (not using .detach() + // to remove the entry since we are replacing it): + (*existingIt)->m_Parent.reset(); + entries().erase(existingIt); + + // insert at the right place + insertionIt = entries().insert( + std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry); + } else { + return end(); + } + } else if ((*existingIt)->isFile() || entry->isFile()) { + // If we arrive here and one of the entry is a file, we fail: + return end(); + } else { + // If we end up here, we know that the policy is MERGE and that both + // are directory that can be merged: + mergeTree((*existingIt)->astree(), entry->astree(), nullptr); + insertionIt = existingIt; + } + } else if (beforeInsert(this, entry.get())) { + insertionIt = entries().insert( + std::lower_bound(begin(), end(), entry, FileEntryComparator{}), entry); + } + + // Remove the tree from its parent (parent() can be null if we are inserting + // a new tree): + if (entry->parent() != nullptr) { + entry->parent()->erase(entry); + } + + // If the entry was actually inserted, we update its parent: + if (*insertionIt == entry) { + entry->m_Parent = astree(); + } + // Otherwize, we reset it (if this was a merge operation): + else { + entry->m_Parent.reset(); + } + + return insertionIt; +} + +/** + * + */ +bool IFileTree::move(std::shared_ptr<FileTreeEntry> entry, QString path, + InsertPolicy insertPolicy) +{ + + // Check that this is not a parent tree: + if (entry->isDir()) { + std::shared_ptr<IFileTree> tmp = parent(); + while (tmp != nullptr) { + if (tmp == entry->astree()) { + return false; + } + tmp = tmp->parent(); + } + } + + // Insert in folder or replace: + const bool insertFolder = path.isEmpty() || path.endsWith("/") || path.endsWith("\\"); + + // Retrieve the path: + QStringList parts = splitPath(path); + + // Backup the entry name (in case the insertion fails), and update the + // name: + QString entryName = entry->m_Name; + if (!insertFolder) { + entry->m_Name = parts.takeLast(); + } + + // Find or create the tree: + IFileTree* tree; + + if (!parts.isEmpty()) { + + // Create the tree: + std::shared_ptr<FileTreeEntry> treeEntry = createTree(parts.begin(), parts.end()); + + // Early fail if the tree was not created: + if (treeEntry == nullptr) { + return false; + } + + tree = treeEntry->astree().get(); + } else { + tree = this; + } + + // We try to insert, and if it fails we need to reset the name: + auto it = tree->insert(entry, insertPolicy); + if (it == tree->end()) { + entry->m_Name = entryName; + return false; + } + + return true; +} + +/** + * + */ +std::shared_ptr<FileTreeEntry> +IFileTree::copy(std::shared_ptr<const FileTreeEntry> entry, QString path, + InsertPolicy insertPolicy) +{ + // Note: If a conflict exists, the tree is cloned before checking the conflict, so + // this is not the most efficient way but copying tree should be pretty rare (and + // probably avoided anyway), and this allow us to use `move()` to do all the complex + // operations. + auto clone = entry->clone(); + if (move(clone, path, insertPolicy)) { + return clone; + } + return nullptr; +} + +/** + * + */ +std::size_t IFileTree::merge(std::shared_ptr<IFileTree> source, + OverwritesType* overwrites) +{ + + // Check that this is not a parent tree: + std::shared_ptr<IFileTree> tmp = astree(); + while (tmp != nullptr) { + if (tmp == source) { + return MERGE_FAILED; + } + tmp = tmp->parent(); + } + + return mergeTree(astree(), source, overwrites); +} + +/** + * + */ +IFileTree::iterator IFileTree::erase(std::shared_ptr<FileTreeEntry> entry) +{ + + if (!beforeRemove(this, entry.get())) { + return end(); + } + + auto it = std::find(begin(), end(), entry); + if (it == end()) { + return it; + } + entry->m_Parent.reset(); + return entries().erase(it); + ; +} + +/** + * + */ +std::pair<IFileTree::iterator, std::shared_ptr<FileTreeEntry>> +IFileTree::erase(QString name) +{ + auto it = std::find_if(begin(), end(), [&name](const auto& entry) { + return entry->compare(name) == 0; + }); + + if (it == end()) { + return {it, nullptr}; + } + + if (!beforeRemove(this, it->get())) { + return {end(), nullptr}; + } + + // Save the entry to return it: + auto entry = *it; + entry->m_Parent.reset(); + + return {entries().erase(it), entry}; +} + +/** + * + */ +bool IFileTree::clear() +{ + // Need to find the iterator up to which we should erase: + auto& entries_ = entries(); + auto it = entries_.begin(); + for (; it != entries_.end() && beforeRemove(this, it->get()); ++it) { + // Detach (but not remove from the vector): + (*it)->m_Parent.reset(); + } + entries_.erase(entries_.begin(), it); + return empty(); +} + +/** + * + */ +std::size_t IFileTree::removeAll(QStringList names) +{ + return removeIf([&names](auto& entry) { + return names.contains(entry->name(), Qt::CaseInsensitive); + }); +} + +/** + * + */ +std::size_t IFileTree::removeIf( + std::function<bool(std::shared_ptr<FileTreeEntry> const&)> predicate) +{ + std::size_t osize = size(); + auto& en = entries(); + // Cannot use begin() and end() directly because those are immutable iterators: + en.erase(std::remove_if(en.begin(), en.end(), + [this, &predicate](auto& entry) { + return beforeRemove(this, entry.get()) && predicate(entry); + }), + en.end()); + return osize - size(); +} + +/** + * + */ +QStringList IFileTree::splitPath(QString path) +{ + // Using raw \\ instead of QDir::separator() since we are replacing by / + // anyway, and this avoid pulling an extra header (like QDir) only + // for the separator. + return path.replace("\\", "/").split("/", Qt::SkipEmptyParts); +} + +/** + * + */ +bool IFileTree::beforeReplace(IFileTree const*, FileTreeEntry const*, + FileTreeEntry const*) +{ + return true; +} + +/** + * + */ +bool IFileTree::beforeInsert(IFileTree const*, FileTreeEntry const*) +{ + return true; +} + +/** + * + */ +bool IFileTree::beforeRemove(IFileTree const*, FileTreeEntry const*) +{ + return true; +} + +/** + * + */ +std::size_t IFileTree::mergeTree(std::shared_ptr<IFileTree> destination, + std::shared_ptr<IFileTree> source, + OverwritesType* overwrites) +{ + + const auto comp = FileEntryComparator{}; + + // Number of overwritten entries: + std::size_t noverwrites = 0; + + // Note: Using the iterators from the vector directly since the other ones + // cannot be assigned to. + auto &dstEntries = destination->entries(), &srcEntries = source->entries(); + + // Note: Since entries are not sorted by name but by type (file/directory) + // then name, there is no fast way to check for conflict. + for (auto& srcEntry : srcEntries) { + + // Try to find an exact match (name and type) - This iterator also + // serve to know where the entry should be inserted: + auto dstIt = std::lower_bound(dstEntries.begin(), dstEntries.end(), srcEntry, comp); + + // Exact match found: + if (dstIt != dstEntries.end() && (*dstIt)->compare(srcEntry->name()) == 0 && + (*dstIt)->isFile() == srcEntry->isFile()) { + + // Both directory, we merge: + if ((*dstIt)->isDir() && srcEntry->isDir()) { + noverwrites += mergeTree((*dstIt)->astree(), srcEntry->astree(), overwrites); + + // Detach the entry: + srcEntry->m_Parent.reset(); + } + // Otherwize, check if the source can replace the destination: + else if (beforeReplace(destination.get(), dstIt->get(), srcEntry.get())) { + // Remove the parent: + auto dstEntry = *dstIt; + dstEntry->m_Parent.reset(); + + // Update overwrites information: + noverwrites++; + if (overwrites != nullptr) { + overwrites->insert({dstEntry, srcEntry}); + } + + // Replace the destination: + *dstIt = srcEntry; + srcEntry->m_Parent = destination; + } + // If not, fails: + else { + return MERGE_FAILED; + } + } else { + // If we did not find a match, the only way to check is to look + // through the vector: + auto conflictIt = std::find_if(dstEntries.begin(), dstEntries.end(), + [name = srcEntry->name()](auto const& dstEntry) { + return dstEntry->compare(name) == 0; + }); + + // Conflict (note that here both entries are of different types, so no need to + // check if we replace or merge): + int deleteIndex = -1; + if (conflictIt != dstEntries.end()) { + + // We check if we can replace the entry: + if (!beforeReplace(destination.get(), conflictIt->get(), srcEntry.get())) { + return MERGE_FAILED; + } + + // We need to store the index because the insert() will mess up the + // iterators: + deleteIndex = static_cast<int>(conflictIt - std::begin(dstEntries)); + if (dstIt < conflictIt) { + deleteIndex += 1; + } + + // Detach the conflicting entry (we erase it later, after the insertion): + (*conflictIt)->m_Parent.reset(); + + // Update overwrites information: + noverwrites++; + if (overwrites != nullptr) { + overwrites->insert({*conflictIt, srcEntry}); + } + } + // No conflict, we still have to check if we can insert: + else if (!beforeInsert(destination.get(), srcEntry.get())) { + return MERGE_FAILED; + } + + // Insert the entry using the previous iterator: + dstEntries.insert(dstIt, srcEntry); + + // We delete here: + if (deleteIndex != -1) { + dstEntries.erase(dstEntries.begin() + deleteIndex); + } + + // Update the parent: + srcEntry->m_Parent = destination; + } + } + + // Clear the sources: + srcEntries.clear(); + + return noverwrites; +} + +/** + * + */ +std::shared_ptr<IFileTree> IFileTree::createOrphanTree(QString name) const +{ + auto directory = makeDirectory(nullptr, name); + if (directory != nullptr) { + directory->m_Populated = true; + } + return directory; +} + +/** + * + */ +std::shared_ptr<FileTreeEntry> IFileTree::fetchEntry(QStringList path, + FileTypes matchTypes) +{ + return std::const_pointer_cast<FileTreeEntry>( + const_cast<const IFileTree*>(this)->fetchEntry(path, matchTypes)); +} +std::shared_ptr<const FileTreeEntry> IFileTree::fetchEntry(QStringList const& path, + FileTypes matchTypes) const +{ + // Check to ensure that the path contains at least one element: + if (path.isEmpty()) { + return nullptr; + } + + // Early check: + if (path[path.size() - 1].startsWith("*")) { + return nullptr; + } + + const IFileTree* tree = this; + auto it = std::begin(path); + for (; tree != nullptr && it != std::end(path) - 1; ++it) { + // Special cases: + if (*it == ".") { + continue; + } else if (*it == "..") { + tree = tree->parent().get(); + } else { + // Find the entry at the current level: + auto entryIt = std::find_if(tree->begin(), tree->end(), + MatchEntryComparator{*it, IFileTree::DIRECTORY}); + + // Early exists if the entry does not exist or is not a directory: + if (entryIt == tree->end()) { + tree = nullptr; + } else { + tree = (*entryIt)->astree().get(); + } + } + } + + if (tree == nullptr) { + return nullptr; + } + + // We have the final tree: + auto entryIt = + std::find_if(tree->begin(), tree->end(), MatchEntryComparator{*it, matchTypes}); + auto bIt = tree->end(); + return entryIt == bIt ? nullptr : *entryIt; +} + +/** + * @brief Create a new file under this tree. + * + * @param parent The current tree, without const-qualification. + * @param name Name of the file. + * @param time Modification time of the file. + * + * @return the created file. + */ +std::shared_ptr<FileTreeEntry> +IFileTree::makeFile(std::shared_ptr<const IFileTree> parent, QString name) const +{ + return createFileEntry(parent, name); +} + +/** + * + */ +IFileTree::IFileTree() {} + +/** + * + */ +std::shared_ptr<FileTreeEntry> IFileTree::clone() const +{ + std::shared_ptr<IFileTree> tree = doClone(); + + // Don't copy not populated tree, it is not useful: + if (m_Populated) { + tree->m_Populated = true; + auto& tentries = tree->m_Entries; + for (auto e : entries()) { + auto ce = e->clone(); + ce->m_Parent = tree; + tentries.push_back(ce); + } + } + + return tree; +} + +/** + * + */ +std::shared_ptr<IFileTree> IFileTree::createTree(QStringList::const_iterator begin, + QStringList::const_iterator end) +{ + // The current tree and entry: + std::shared_ptr<IFileTree> tree = astree(); + for (auto it = begin; tree != nullptr && it != end; ++it) { + // Special cases: + if (*it == ".") { + continue; + } else if (*it == "..") { + // parent() returns nullptr if it does not exist, so no + // check required: + tree = parent(); + } else { + + // Check if the entry exists (looking for both files and directories + // because we don't want to override a file): + auto entryIt = + std::find_if(tree->begin(), tree->end(), + MatchEntryComparator{*it, IFileTree::FILE_OR_DIRECTORY}); + + // Create if it does not: + if (entryIt == tree->end()) { + auto newTree = tree->makeDirectory(tree, *it); + + // If makeDirectory returns a null pointer, it means we cannot create tree. + if (newTree == nullptr) { + tree = nullptr; + break; + } + + // The tree is empty so already populated: + newTree->m_Populated = true; + + tree->entries().insert(std::upper_bound(tree->begin(), tree->end(), newTree, + FileEntryComparator{}), + newTree); + tree = newTree; + } else if ((*entryIt)->isDir()) { + tree = (*entryIt)->astree(); + } else { // Cannot go further: + tree = nullptr; + } + } + } + + return tree; +} + +/** + * @brief Retrieve the vector of entries after populating it if required. + * + * @return the vector of entries. + */ +std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries() +{ + std::call_once(m_OnceFlag, [this]() { + populate(); + }); + return m_Entries; +} +const std::vector<std::shared_ptr<FileTreeEntry>>& IFileTree::entries() const +{ + std::call_once(m_OnceFlag, [this]() { + populate(); + }); + return m_Entries; +} + +/** + * @brief Populate the internal vectors and update the flag. + */ +void IFileTree::populate() const +{ + // Need to check m_Populated again here since the tree can be populated without + // a call to entries() (e.g., on copy/orphanTree): + if (!m_Populated) { + if (!doPopulate(astree(), m_Entries)) { + std::sort(std::begin(m_Entries), std::end(m_Entries), FileEntryComparator{}); + } + m_Populated = true; + } +} + +// walk and glob with generator + +std::generator<std::shared_ptr<const FileTreeEntry>> +walk(std::shared_ptr<const IFileTree> fileTree) +{ + std::stack<std::shared_ptr<const FileTreeEntry>> stack; + + // we start by pushing all the entries in this tree, this avoid having to do extra + // check later to avoid leading separator + for (auto rit = fileTree->rbegin(); rit != fileTree->rend(); ++rit) { + stack.push(*rit); + } + + while (!stack.empty()) { + auto entry = stack.top(); + stack.pop(); + + co_yield entry; + + if (entry->isDir()) { + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + stack.push(*rit); + } + } + } +} + +namespace +{ + using glob_stack_t = std::stack< + std::pair<std::shared_ptr<const FileTreeEntry>, std::span<QRegularExpression>>>; + + // check the given entry against the list of patterns and add new (entry, patterns) to + // the given stack + // + std::generator<std::shared_ptr<const FileTreeEntry>> + ifiletree_glob_impl_step(glob_stack_t& glob_stack, + std::shared_ptr<const FileTreeEntry> entry, + std::span<QRegularExpression> const& patterns) + { + // no more patterns, nothing to do + if (patterns.size() == 0) { + co_return; + } + + // special handling if the pattern is empty (correspond to a '**' glob) + if (patterns[0].pattern() == "") { + + // if there are more patterns after '**', we need to check the entry again, e.g., + // if the entry name is 'x' and the pattern is '**/x' to match it + if (patterns.size() != 1) { + glob_stack.emplace(entry, patterns.subspan(1)); + } + + // if the entry is a file, there is nothing to do with '**' + if (entry->isFile()) { + co_return; + } + + // if this is the end of the patterns list, we need to yield the current entry + // since it is a directory + if (patterns.size() == 1) { + co_yield entry; + } + + // recurse over childs, but for directories, we need to keep the leading '**' in + // the list of patterns since '**' can match multiple level of directories + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + glob_stack.emplace(*rit, (*rit)->isDir() ? patterns : patterns.subspan(1)); + } + } + // otherwise (if the first patterns is not '**'), we simply check if we have a match + else if (patterns[0].match(entry->name()).hasMatch()) { + // this was the last pattern and we have a match, so we yield the current entry, + // not that this will yield intermediate matching directory, but this is expected + if (patterns.size() == 1) { + co_yield entry; + } + + // if the entry is not a directory, we have nothing more to do + if (entry->isFile()) { + co_return; + } + + // if all that remain after this pattern is a '**', we need to yield the current + // entry since '**' can also match an empty succession of directories, e.g. 'a/b' + // is matched by 'a/b/**' + if (patterns.size() == 2 && patterns[1].pattern() == "") { + co_yield entry; + } + + // we then need to recurse over + auto tree = entry->astree(); + for (auto rit = tree->rbegin(); rit != tree->rend(); ++rit) { + glob_stack.emplace(*rit, patterns.subspan(1)); + } + } + } + + // main function for IFileTree::glob - this function simply manages the stack of of + // entry / patterns to handle, and then falls back to ifiletree_glob_impl_step + // + std::generator<std::shared_ptr<const FileTreeEntry>> + ifiletree_glob_impl(std::shared_ptr<const FileTreeEntry> entry, + std::span<QRegularExpression> const& patterns) + { + glob_stack_t stack; + stack.emplace(entry, patterns); + + while (!stack.empty()) { + const auto [childEntry, childPatterns] = stack.top(); + stack.pop(); + co_yield std::ranges::elements_of( + ifiletree_glob_impl_step(stack, childEntry, childPatterns)); + } + } + +} // namespace + +/** + * + */ +std::generator<std::shared_ptr<const FileTreeEntry>> +glob(std::shared_ptr<const IFileTree> fileTree, QString pattern, + GlobPatternType patternType) +{ + // replace \\ by / to simply handling + pattern = pattern.trimmed().replace("\\", "/"); + + // reduce successions of **/** to **, this makes it easier to handle it in the + // actual implementation + pattern = pattern.replace(QRegularExpression("(\\*\\*/)*\\*\\*"), "**"); + + // we are going to match directly starting from the child of the tree, so we need + // to handle the root here + // + // note: this is the only pattern that can match the tree itself + if (pattern == "**") { + co_yield fileTree; + } + + // split pattern into blocks, we keep + const auto regexOptions = FileNameComparator::CaseSensitivity == Qt::CaseInsensitive + ? QRegularExpression::CaseInsensitiveOption + : QRegularExpression::NoPatternOption; + std::vector<QRegularExpression> patterns; + for (const auto& part : pattern.split("/")) { + if (part == "**") { + // for '**' pattern, push an empty regex + patterns.emplace_back(); + } else { + const auto regexpPattern = + patternType == GlobPatternType::GLOB + ? QRegularExpression::wildcardToRegularExpression( + part, QRegularExpression::NonPathWildcardConversion) + : part; + const QRegularExpression regex(regexpPattern, regexOptions); + + if (!regex.isValid()) { + throw InvalidGlobPatternException(regex.errorString()); + } + + patterns.push_back(regex); + } + } + + // fall back to the main glob function for the child of this tree + for (const auto& child : *fileTree) { + co_yield std::ranges::elements_of(ifiletree_glob_impl(child, patterns)); + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/imodrepositorybridge.cpp b/libs/uibase/src/imodrepositorybridge.cpp new file mode 100644 index 0000000..9b610b0 --- /dev/null +++ b/libs/uibase/src/imodrepositorybridge.cpp @@ -0,0 +1 @@ +#include <uibase/imodrepositorybridge.h> diff --git a/libs/uibase/src/imoinfo.cpp b/libs/uibase/src/imoinfo.cpp new file mode 100644 index 0000000..faa2db9 --- /dev/null +++ b/libs/uibase/src/imoinfo.cpp @@ -0,0 +1,23 @@ +#include <uibase/imoinfo.h> + +namespace MOBase +{ + +static QString g_pluginDataPath; + +QString IOrganizer::getPluginDataPath() +{ + return g_pluginDataPath; +} + +} // namespace MOBase + +namespace MOBase::details +{ + +void setPluginDataPath(const QString& s) +{ + g_pluginDataPath = s; +} + +} // namespace MOBase::details diff --git a/libs/uibase/src/json.cpp b/libs/uibase/src/json.cpp new file mode 100644 index 0000000..49d5fb3 --- /dev/null +++ b/libs/uibase/src/json.cpp @@ -0,0 +1,557 @@ +/** + * QtJson - A simple class for parsing JSON data into a QVariant hierarchies and + * vice-versa. Copyright (C) 2011 Eeli Reilin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * \file json.cpp + */ + +#include <uibase/json.h> + +namespace QtJson +{ +static QString sanitizeString(QString str); +static QByteArray join(const QList<QByteArray>& list, const QByteArray& sep); +static QVariant parseValue(const QString& json, int& index, bool& success); +static QVariant parseObject(const QString& json, int& index, bool& success); +static QVariant parseArray(const QString& json, int& index, bool& success); +static QVariant parseString(const QString& json, int& index, bool& success); +static QVariant parseNumber(const QString& json, int& index); +static int lastIndexOfNumber(const QString& json, int index); +static void eatWhitespace(const QString& json, int& index); +static int lookAhead(const QString& json, int index); +static int nextToken(const QString& json, int& index); + +template <typename T> +QByteArray serializeMap(const T& map, bool& success) +{ + QByteArray str = "{ "; + QList<QByteArray> pairs; + for (typename T::const_iterator it = map.begin(), itend = map.end(); it != itend; + ++it) { + QByteArray serializedValue = serialize(it.value()); + if (serializedValue.isNull()) { + success = false; + break; + } + pairs << sanitizeString(it.key()).toUtf8() + " : " + serializedValue; + } + + str += join(pairs, ", "); + str += " }"; + return str; +} + +/** + * parse + */ +QVariant parse(const QString& json) +{ + bool success = true; + return parse(json, success); +} + +/** + * parse + */ +QVariant parse(const QString& json, bool& success) +{ + success = true; + + // Return an empty QVariant if the JSON data is either null or empty + if (!json.isNull() || !json.isEmpty()) { + QString data = json; + // We'll start from index 0 + int index = 0; + + // Parse the first value + QVariant value = parseValue(data, index, success); + + // Return the parsed value + return value; + } else { + // Return the empty QVariant + return QVariant(); + } +} + +QByteArray serialize(const QVariant& data) +{ + bool success = true; + return serialize(data, success); +} + +QByteArray serialize(const QVariant& data, bool& success) +{ + QByteArray str; + success = true; + + if (!data.isValid()) { // invalid or null? + str = "null"; + } else if ((data.typeId() == QMetaType::Type::QVariantList) || + (data.typeId() == QMetaType::Type::QStringList)) { // variant is a list? + QList<QByteArray> values; + const QVariantList list = data.toList(); + Q_FOREACH (const QVariant& v, list) { + QByteArray serializedValue = serialize(v); + if (serializedValue.isNull()) { + success = false; + break; + } + values << serializedValue; + } + + str = "[ " + join(values, ", ") + " ]"; + } else if (data.typeId() == QMetaType::Type::QVariantHash) { // variant is a hash? + str = serializeMap<>(data.toHash(), success); + } else if (data.typeId() == QMetaType::Type::QVariantMap) { // variant is a map? + str = serializeMap<>(data.toMap(), success); + } else if ((data.typeId() == QMetaType::Type::QString) || + (data.typeId() == + QMetaType::Type::QByteArray)) { // a string or a byte array? + str = sanitizeString(data.toString()).toUtf8(); + } else if (data.typeId() == QMetaType::Type::Double) { // double? + double value = data.toDouble(); + if ((value - value) == 0.0) { + str = QByteArray::number(value, 'g'); + if (!str.contains(".") && !str.contains("e")) { + str += ".0"; + } + } else { + success = false; + } + } else if (data.typeId() == QMetaType::Type::Bool) { // boolean value? + str = data.toBool() ? "true" : "false"; + } else if (data.typeId() == QMetaType::Type::ULongLong) { // large unsigned number? + str = QByteArray::number(data.value<qulonglong>()); + } else if (data.canConvert<qlonglong>()) { // any signed number? + str = QByteArray::number(data.value<qlonglong>()); + } else if (data.canConvert<long>()) { // TODO: this code is never executed + str = QString::number(data.value<long>()).toUtf8(); + } else if (data.canConvert<QString>()) { // can value be converted to string? + // this will catch QDate, QDateTime, QUrl, ... + str = sanitizeString(data.toString()).toUtf8(); + } else { + success = false; + } + + if (success) { + return str; + } else { + return QByteArray(); + } +} + +QString serializeStr(const QVariant& data) +{ + return QString::fromUtf8(serialize(data)); +} + +QString serializeStr(const QVariant& data, bool& success) +{ + return QString::fromUtf8(serialize(data, success)); +} + +/** + * \enum JsonToken + */ +enum JsonToken +{ + JsonTokenNone = 0, + JsonTokenCurlyOpen = 1, + JsonTokenCurlyClose = 2, + JsonTokenSquaredOpen = 3, + JsonTokenSquaredClose = 4, + JsonTokenColon = 5, + JsonTokenComma = 6, + JsonTokenString = 7, + JsonTokenNumber = 8, + JsonTokenTrue = 9, + JsonTokenFalse = 10, + JsonTokenNull = 11 +}; + +static QString sanitizeString(QString str) +{ + str.replace(QLatin1String("\\"), QLatin1String("\\\\")); + str.replace(QLatin1String("\""), QLatin1String("\\\"")); + str.replace(QLatin1String("\b"), QLatin1String("\\b")); + str.replace(QLatin1String("\f"), QLatin1String("\\f")); + str.replace(QLatin1String("\n"), QLatin1String("\\n")); + str.replace(QLatin1String("\r"), QLatin1String("\\r")); + str.replace(QLatin1String("\t"), QLatin1String("\\t")); + return QString(QLatin1String("\"%1\"")).arg(str); +} + +static QByteArray join(const QList<QByteArray>& list, const QByteArray& sep) +{ + QByteArray res; + Q_FOREACH (const QByteArray& i, list) { + if (!res.isEmpty()) { + res += sep; + } + res += i; + } + return res; +} + +/** + * parseValue + */ +static QVariant parseValue(const QString& json, int& index, bool& success) +{ + // Determine what kind of data we should parse by + // checking out the upcoming token + switch (lookAhead(json, index)) { + case JsonTokenString: + return parseString(json, index, success); + case JsonTokenNumber: + return parseNumber(json, index); + case JsonTokenCurlyOpen: + return parseObject(json, index, success); + case JsonTokenSquaredOpen: + return parseArray(json, index, success); + case JsonTokenTrue: + nextToken(json, index); + return QVariant(true); + case JsonTokenFalse: + nextToken(json, index); + return QVariant(false); + case JsonTokenNull: + nextToken(json, index); + return QVariant(); + case JsonTokenNone: + break; + } + + // If there were no tokens, flag the failure and return an empty QVariant + success = false; + return QVariant(); +} + +/** + * parseObject + */ +static QVariant parseObject(const QString& json, int& index, bool& success) +{ + QVariantMap map; + int token; + + // Get rid of the whitespace and increment index + nextToken(json, index); + + // Loop through all of the key/value pairs of the object + bool done = false; + while (!done) { + // Get the upcoming token + token = lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantMap(); + } else if (token == JsonTokenComma) { + nextToken(json, index); + } else if (token == JsonTokenCurlyClose) { + nextToken(json, index); + return map; + } else { + // Parse the key/value pair's name + QString name = parseString(json, index, success).toString(); + + if (!success) { + return QVariantMap(); + } + + // Get the next token + token = nextToken(json, index); + + // If the next token is not a colon, flag the failure + // return an empty QVariant + if (token != JsonTokenColon) { + success = false; + return QVariant(QVariantMap()); + } + + // Parse the key/value pair's value + QVariant value = parseValue(json, index, success); + + if (!success) { + return QVariantMap(); + } + + // Assign the value to the key in the map + map[name] = value; + } + } + + // Return the map successfully + return QVariant(map); +} + +/** + * parseArray + */ +static QVariant parseArray(const QString& json, int& index, bool& success) +{ + QVariantList list; + + nextToken(json, index); + + bool done = false; + while (!done) { + int token = lookAhead(json, index); + + if (token == JsonTokenNone) { + success = false; + return QVariantList(); + } else if (token == JsonTokenComma) { + nextToken(json, index); + } else if (token == JsonTokenSquaredClose) { + nextToken(json, index); + break; + } else { + QVariant value = parseValue(json, index, success); + if (!success) { + return QVariantList(); + } + list.push_back(value); + } + } + + return QVariant(list); +} + +/** + * parseString + */ +static QVariant parseString(const QString& json, int& index, bool& success) +{ + QString s; + QChar c; + + eatWhitespace(json, index); + + c = json[index++]; + + bool complete = false; + while (!complete) { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + complete = true; + break; + } else if (c == '\\') { + if (index == json.size()) { + break; + } + + c = json[index++]; + + if (c == '\"') { + s.append('\"'); + } else if (c == '\\') { + s.append('\\'); + } else if (c == '/') { + s.append('/'); + } else if (c == 'b') { + s.append('\b'); + } else if (c == 'f') { + s.append('\f'); + } else if (c == 'n') { + s.append('\n'); + } else if (c == 'r') { + s.append('\r'); + } else if (c == 't') { + s.append('\t'); + } else if (c == 'u') { + qsizetype remainingLength = json.size() - index; + if (remainingLength >= 4) { + QString unicodeStr = json.mid(index, 4); + + int symbol = unicodeStr.toInt(0, 16); + + s.append(QChar(symbol)); + + index += 4; + } else { + break; + } + } + } else { + s.append(c); + } + } + + if (!complete) { + success = false; + return QVariant(); + } + + return QVariant(s); +} + +/** + * parseNumber + */ +static QVariant parseNumber(const QString& json, int& index) +{ + eatWhitespace(json, index); + + int lastIndex = lastIndexOfNumber(json, index); + int charLength = (lastIndex - index) + 1; + QString numberStr; + + numberStr = json.mid(index, charLength); + + index = lastIndex + 1; + bool ok; + + if (numberStr.contains('.')) { + return QVariant(numberStr.toDouble(nullptr)); + } else if (numberStr.startsWith('-')) { + int i = numberStr.toInt(&ok); + if (!ok) { + qlonglong ll = numberStr.toLongLong(&ok); + return ok ? ll : QVariant(numberStr); + } + return i; + } else { + uint u = numberStr.toUInt(&ok); + if (!ok) { + qulonglong ull = numberStr.toULongLong(&ok); + return ok ? ull : QVariant(numberStr); + } + return u; + } +} + +/** + * lastIndexOfNumber + */ +static int lastIndexOfNumber(const QString& json, int index) +{ + int lastIndex; + + for (lastIndex = index; lastIndex < json.size(); lastIndex++) { + if (QString("0123456789+-.eE").indexOf(json[lastIndex]) == -1) { + break; + } + } + + return lastIndex - 1; +} + +/** + * eatWhitespace + */ +static void eatWhitespace(const QString& json, int& index) +{ + for (; index < json.size(); index++) { + if (QString(" \t\n\r").indexOf(json[index]) == -1) { + break; + } + } +} + +/** + * lookAhead + */ +static int lookAhead(const QString& json, int index) +{ + int saveIndex = index; + return nextToken(json, saveIndex); +} + +/** + * nextToken + */ +static int nextToken(const QString& json, int& index) +{ + eatWhitespace(json, index); + + if (index == json.size()) { + return JsonTokenNone; + } + + QChar c = json[index]; + index++; + switch (c.toLatin1()) { + case '{': + return JsonTokenCurlyOpen; + case '}': + return JsonTokenCurlyClose; + case '[': + return JsonTokenSquaredOpen; + case ']': + return JsonTokenSquaredClose; + case ',': + return JsonTokenComma; + case '"': + return JsonTokenString; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return JsonTokenNumber; + case ':': + return JsonTokenColon; + } + index--; // ^ WTF? + + qsizetype remainingLength = json.size() - index; + + // True + if (remainingLength >= 4) { + if (json[index] == 't' && json[index + 1] == 'r' && json[index + 2] == 'u' && + json[index + 3] == 'e') { + index += 4; + return JsonTokenTrue; + } + } + + // False + if (remainingLength >= 5) { + if (json[index] == 'f' && json[index + 1] == 'a' && json[index + 2] == 'l' && + json[index + 3] == 's' && json[index + 4] == 'e') { + index += 5; + return JsonTokenFalse; + } + } + + // Null + if (remainingLength >= 4) { + if (json[index] == 'n' && json[index + 1] == 'u' && json[index + 2] == 'l' && + json[index + 3] == 'l') { + index += 4; + return JsonTokenNull; + } + } + + return JsonTokenNone; +} +} // namespace QtJson diff --git a/libs/uibase/src/lineeditclear.cpp b/libs/uibase/src/lineeditclear.cpp new file mode 100644 index 0000000..476c874 --- /dev/null +++ b/libs/uibase/src/lineeditclear.cpp @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (c) 2007 Trolltech ASA <info@trolltech.com> +** +** Use, modification and distribution is allowed without limitation, +** warranty, liability or support of any kind. +** +****************************************************************************/ + +#include <uibase/lineeditclear.h> +#include <QStyle> +#include <QToolButton> + +namespace MOBase +{ + +LineEditClear::LineEditClear(QWidget* parent) : QLineEdit(parent) +{ + clearButton = new QToolButton(this); + QPixmap pixmap(":/MO/gui/edit_clear"); + clearButton->setIcon(QIcon(pixmap)); + clearButton->setIconSize(pixmap.size()); + clearButton->setCursor(Qt::ArrowCursor); + clearButton->setStyleSheet("QToolButton { border: none; padding: 0px; }"); + clearButton->hide(); + connect(clearButton, SIGNAL(clicked()), this, SLOT(clear())); + connect(this, SIGNAL(textChanged(const QString&)), this, + SLOT(updateCloseButton(const QString&))); + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + setStyleSheet(QString("QLineEdit { padding-right: %1px; } ") + .arg(clearButton->sizeHint().width() + frameWidth + 1)); + /* QSize msz = minimumSizeHint(); + setMinimumSize(qMax(msz.width(), clearButton->sizeHint().height() + frameWidth * 2 + + 2), qMax(msz.height(), clearButton->sizeHint().height() + frameWidth * 2 + 2));*/ +} + +void LineEditClear::resizeEvent(QResizeEvent*) +{ + QSize sz = clearButton->sizeHint(); + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + clearButton->move(rect().right() - frameWidth - sz.width(), + (rect().bottom() + 1 - sz.height()) / 2); +} + +void LineEditClear::updateCloseButton(const QString& text) +{ + clearButton->setVisible(!text.isEmpty()); +} + +} // namespace MOBase diff --git a/libs/uibase/src/linklabel.cpp b/libs/uibase/src/linklabel.cpp new file mode 100644 index 0000000..c4d09f1 --- /dev/null +++ b/libs/uibase/src/linklabel.cpp @@ -0,0 +1,27 @@ +#include <uibase/linklabel.h> +#include <QApplication> + +QColor LinkLabel::m_linkColor; + +LinkLabel::LinkLabel(QWidget* parent) : QLabel(parent) {} + +QColor LinkLabel::linkColor() const +{ + return m_linkColor; +} + +void LinkLabel::setLinkColor(const QColor& c) +{ + if (m_linkColor != c) { + m_linkColor = c; + + // setting link color on the global palette; qt doesn't seem to support + // per-widget colors for links + if (qApp) { + auto p = qApp->palette(); + p.setColor(QPalette::Link, c); + p.setColor(QPalette::LinkVisited, c); + qApp->setPalette(p); + } + } +} diff --git a/libs/uibase/src/log.cpp b/libs/uibase/src/log.cpp new file mode 100644 index 0000000..a44c9e3 --- /dev/null +++ b/libs/uibase/src/log.cpp @@ -0,0 +1,411 @@ +#include <uibase/log.h> +#include "pch.h" +#include <uibase/utility.h> +#include <iostream> + +#include <algorithm> +#include <locale> + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4365) +#endif + +#ifdef _WIN32 +#define SPDLOG_WCHAR_FILENAMES 1 +#endif + +#include <spdlog/logger.h> +#include <spdlog/sinks/base_sink.h> +#include <spdlog/sinks/basic_file_sink.h> +#include <spdlog/sinks/daily_file_sink.h> +#include <spdlog/sinks/dist_sink.h> +#include <spdlog/sinks/rotating_file_sink.h> +#include <spdlog/sinks/stdout_color_sinks.h> + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +namespace MOBase::log +{ + +namespace fs = std::filesystem; +static std::unique_ptr<Logger> g_default; + +spdlog::level::level_enum toSpdlog(Levels lv) +{ + switch (lv) { + case Debug: + return spdlog::level::debug; + + case Warning: + return spdlog::level::warn; + + case Error: + return spdlog::level::err; + + case Info: // fall-through + default: + return spdlog::level::info; + } +} + +Levels fromSpdlog(spdlog::level::level_enum lv) +{ + switch (lv) { + case spdlog::level::trace: + case spdlog::level::debug: + return Debug; + + case spdlog::level::warn: + return Warning; + + case spdlog::level::critical: // fall-through + case spdlog::level::err: + return Error; + + case spdlog::level::info: // fall-through + case spdlog::level::off: + case spdlog::level::n_levels: // to please MSVC + default: + return Info; + } +} + +class CallbackSink : public spdlog::sinks::base_sink<std::mutex> +{ +public: + CallbackSink(Callback* f) : m_f(f) {} + + void setCallback(Callback* f) { m_f = f; } + +protected: + void sink_it_(const spdlog::details::log_msg& m) override + { + thread_local bool active = false; + + if (active) { + // trying to log from a log callback, ignoring + return; + } + + if (!m_f) { + // disabled + return; + } + + try { + auto g = Guard([&] { + active = false; + }); + active = true; + + Entry e; + e.time = m.time; + e.level = fromSpdlog(m.level); + e.message = std::string(m.payload); + + spdlog::memory_buf_t formatted; + base_sink::formatter_->format(m, formatted); + + if (formatted.size() >= 2) { + // remove \r\n + e.formattedMessage.assign(formatted.begin(), formatted.end() - 2); + } else { + e.formattedMessage = std::string(formatted); + } + + (*m_f)(std::move(e)); + } catch (std::exception& e) { + fprintf(stderr, "uncaugh exception in logging callback, %s\n", e.what()); + } catch (...) { + fprintf(stderr, "uncaught exception in logging callback\n"); + } + } + + void flush_() override + { + // no-op + } + +private: + std::atomic<Callback*> m_f; +}; + +File::File() : type(None), maxSize(0), maxFiles(0), dailyHour(0), dailyMinute(0) {} + +File File::daily(fs::path file, int hour, int minute) +{ + File fl; + + fl.type = Daily; + fl.file = std::move(file); + fl.dailyHour = hour; + fl.dailyMinute = minute; + + return fl; +} + +File File::rotating(fs::path file, std::size_t maxSize, std::size_t maxFiles) +{ + File fl; + + fl.type = Rotating; + fl.file = std::move(file); + fl.maxSize = maxSize; + fl.maxFiles = maxFiles; + + return fl; +} + +File File::single(std::filesystem::path file) +{ + File fl; + + fl.type = Single; + fl.file = std::move(file); + + return fl; +} + +spdlog::sink_ptr createFileSink(const File& f) +{ + try { + switch (f.type) { + case File::Daily: { + return std::make_shared<spdlog::sinks::daily_file_sink_mt>( + f.file.native(), f.dailyHour, f.dailyMinute); + } + + case File::Rotating: { + return std::make_shared<spdlog::sinks::rotating_file_sink_mt>( + f.file.native(), f.maxSize, f.maxFiles); + } + + case File::Single: { + return std::make_shared<spdlog::sinks::basic_file_sink_mt>(f.file.native(), true); + } + + case File::None: // fall-through + default: + return {}; + } + } catch (spdlog::spdlog_ex& e) { + std::cerr << "failed to create file log, " << e.what() << "\n"; + return {}; + } +} + +Logger::Logger(LoggerConfiguration conf_moved) : m_conf(std::move(conf_moved)) +{ + createLogger(m_conf.name); + + const auto timeType = + m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local; + + m_logger->set_level(toSpdlog(m_conf.maxLevel)); + m_logger->set_pattern(m_conf.pattern, timeType); + m_logger->flush_on(spdlog::level::trace); +} + +// anchor +Logger::~Logger() = default; + +Levels Logger::level() const +{ + return fromSpdlog(m_logger->level()); +} + +void Logger::setLevel(Levels lv) +{ + m_logger->set_level(toSpdlog(lv)); +} + +void Logger::setPattern(const std::string& s) +{ + m_logger->set_pattern(s); +} + +void Logger::setFile(const File& f) +{ + + if (m_file) { + auto* ds = static_cast<spdlog::sinks::dist_sink<std::mutex>*>(m_sinks.get()); + ds->remove_sink(m_file); + m_file = {}; + } + + if (f.type != File::None) { + try { + m_file = createFileSink(f); + + if (m_file) { + addSink(m_file); + } + } catch (spdlog::spdlog_ex& e) { + error("{}", e.what()); + } + } +} + +void Logger::setCallback(Callback* f) +{ + if (m_callback) { + static_cast<CallbackSink*>(m_callback.get())->setCallback(f); + } else { + m_callback.reset(new CallbackSink(f)); + addSink(m_callback); + } +} + +void Logger::addToBlacklist(const std::string& filter, const std::string& replacement) +{ + if (filter.length() <= 0 || replacement.length() <= 0) { + // nothing to do + return; + } + + bool present = false; + for (BlacklistEntry& e : m_conf.blacklist) { + if (iequals(e.filter, filter)) { + e.replacement = replacement; + present = true; + break; + } + } + if (!present) { + m_conf.blacklist.push_back(BlacklistEntry(filter, replacement)); + } +} + +void Logger::removeFromBlacklist(const std::string& filter) +{ + if (filter.length() <= 0) { + // nothing to do + return; + } + + for (auto it = m_conf.blacklist.begin(); it != m_conf.blacklist.end();) { + if (iequals(it->filter, filter)) { + it = m_conf.blacklist.erase(it); + } else { + ++it; + } + } +} + +void Logger::resetBlacklist() +{ + m_conf.blacklist.clear(); +} + +void Logger::createLogger(const std::string& name) +{ + m_sinks.reset(new spdlog::sinks::dist_sink<std::mutex>); + +#ifdef _WIN32 + DWORD console_mode; + if (::GetConsoleMode(::GetStdHandle(STD_ERROR_HANDLE), &console_mode) != 0) { + using sink_type = spdlog::sinks::wincolor_stderr_sink_mt; + m_console.reset(new sink_type); + + if (auto* cs = dynamic_cast<sink_type*>(m_console.get())) { + cs->set_color(spdlog::level::info, + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + cs->set_color(spdlog::level::debug, + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + } + + addSink(m_console); + } +#else + // On Linux, use ANSI color stderr sink + m_console.reset(new spdlog::sinks::ansicolor_stderr_sink_mt); + addSink(m_console); +#endif + + m_logger.reset(new spdlog::logger(name, m_sinks)); +} + +void Logger::addSink(std::shared_ptr<spdlog::sinks::sink> sink) +{ + // this is called for both the file and callback sinks + // + // in createLogger(), the dist_sink that was just created will be given the + // pattern that was set in Logger::Logger(), and will pass it to its children; + // the log level is irrelevant in child sinks because dist_sink checks it + // itself + // + // the problem then is that dist_sink doesn't have children yet, they're added + // in setFile() and setCallback(), which can be called by the user much later + // (or not at all) + // + // however, when a sink is added to dist_sink, it does _not_ set the pattern + // on it, it merely adds it to the list + // + // this sets the formatter on the sink manually before adding it to dist_sink + + auto* ds = static_cast<spdlog::sinks::dist_sink<std::mutex>*>(m_sinks.get()); + + const auto timeType = + m_conf.utc ? spdlog::pattern_time_type::utc : spdlog::pattern_time_type::local; + + sink->set_formatter( + std::make_unique<spdlog::pattern_formatter>(m_conf.pattern, timeType)); + + ds->add_sink(sink); +} + +QString levelToString(Levels level) +{ + const auto spdlogLevel = toSpdlog(level); + const auto sv = spdlog::level::to_string_view(spdlogLevel); + const std::string s(sv.begin(), sv.end()); + + return QString::fromStdString(s); +} + +void createDefault(LoggerConfiguration conf) +{ + g_default = std::make_unique<Logger>(conf); +} + +Logger& getDefault() +{ + Q_ASSERT(g_default); + return *g_default; +} + +} // namespace MOBase::log + +namespace MOBase::log::details +{ + +void doLogImpl(spdlog::logger& lg, Levels lv, const std::string& s) noexcept +{ + try { + const char* start = s.c_str(); + const char* p = start; + + for (;;) { + while (*p && *p != '\n') { + ++p; + } + + std::string_view sv(start, static_cast<std::size_t>(p - start)); + lg.log(toSpdlog(lv), "{}", sv); + + if (!*p) { + break; + } + + ++p; + start = p; + } + } catch (...) { + // eat it + } +} + +} // namespace MOBase::log::details diff --git a/libs/uibase/src/modrepositoryfileinfo.cpp b/libs/uibase/src/modrepositoryfileinfo.cpp new file mode 100644 index 0000000..2b0f729 --- /dev/null +++ b/libs/uibase/src/modrepositoryfileinfo.cpp @@ -0,0 +1,81 @@ +#include <uibase/modrepositoryfileinfo.h> +#include <uibase/json.h> + +MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo( + const ModRepositoryFileInfo& reference) + : QObject(reference.parent()), name(reference.name), uri(reference.uri), + description(reference.description), version(reference.version), + categoryID(reference.categoryID), modName(reference.modName), + gameName(reference.gameName), modID(reference.modID), fileID(reference.fileID), + fileSize(reference.fileSize), fileCategory(reference.fileCategory), + repository(reference.repository), userData(reference.userData), + author(reference.author), uploader(reference.uploader), + uploaderUrl(reference.uploaderUrl) +{} + +MOBase::ModRepositoryFileInfo::ModRepositoryFileInfo(QString gameName, int modID, + int fileID) + : name(), uri(), description(), version(), categoryID(0), modName(), + gameName(gameName), modID(modID), fileID(fileID), fileSize(0), + fileCategory(TYPE_UNKNOWN), repository(), userData(), author(), uploader(), + uploaderUrl() + +{} + +MOBase::ModRepositoryFileInfo +MOBase::ModRepositoryFileInfo::createFromJson(const QString& data) +{ + QVariantList result = QtJson::parse(data).toList(); + + while (result.length() < 18) { + result.append(QVariant()); + } + + ModRepositoryFileInfo newInfo; + + newInfo.gameName = result.at(0).toString(); + newInfo.fileID = result.at(1).toInt(); + newInfo.name = result.at(2).toString(); + newInfo.uri = result.at(3).toString(); + newInfo.version.parse(result.at(4).toString()); + newInfo.description = result.at(5).toString(); + newInfo.categoryID = result.at(6).toInt(); + newInfo.fileSize = result.at(7).toUInt(); + newInfo.modID = result.at(8).toInt(); + newInfo.modName = result.at(9).toString(); + newInfo.newestVersion.parse(result.at(10).toString()); + newInfo.fileName = result.at(11).toString(); + newInfo.fileCategory = result.at(12).toInt(); + newInfo.repository = result.at(13).toString(); + newInfo.userData = result.at(14).toMap(); + newInfo.author = result.at(15).toString(); + newInfo.uploader = result.at(16).toString(); + newInfo.uploaderUrl = result.at(17).toString(); + + return newInfo; +} + +QString MOBase::ModRepositoryFileInfo::toString() const +{ + return QString("[ " + "\"%1\",%2,\"%3\",\"%4\",\"%5\",\"%6\",%7,%8,%9,\"%10\",\"%11\",\"%" + "12\",%13,\"%14\",%15 ]") + .arg(gameName) + .arg(fileID) + .arg(name) + .arg(uri) + .arg(version.canonicalString()) + .arg(description.mid(0).replace("\"", "'")) + .arg(categoryID) + .arg(fileSize) + .arg(modID) + .arg(modName) + .arg(newestVersion.canonicalString()) + .arg(fileName) + .arg(fileCategory) + .arg(repository) + .arg(QString(QtJson::serialize(userData))) + .arg(author) + .arg(uploader) + .arg(uploaderUrl); +} diff --git a/libs/uibase/src/nxmurl.cpp b/libs/uibase/src/nxmurl.cpp new file mode 100644 index 0000000..d09cdde --- /dev/null +++ b/libs/uibase/src/nxmurl.cpp @@ -0,0 +1,44 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <uibase/nxmurl.h> +#include <uibase/utility.h> +#include <QList> +#include <QRegularExpression> +#include <QString> +#include <QUrl> +#include <QUrlQuery> + +NXMUrl::NXMUrl(const QString& url) +{ + QUrl nxm(url); + QUrlQuery query(nxm); + QRegularExpression exp("nxm://[a-z0-9]+/mods/(\\d+)/files/(\\d+)", + QRegularExpression::CaseInsensitiveOption); + auto match = exp.match(url); + if (!match.hasMatch()) { + throw MOBase::InvalidNXMLinkException(url); + } + m_Game = nxm.host(); + m_ModId = match.captured(1).toInt(); + m_FileId = match.captured(2).toInt(); + m_Key = query.queryItemValue("key"); + m_Expires = query.queryItemValue("expires").toInt(); + m_UserId = query.queryItemValue("user_id").toInt(); +} diff --git a/libs/uibase/src/pch.cpp b/libs/uibase/src/pch.cpp new file mode 100644 index 0000000..1d9f38c --- /dev/null +++ b/libs/uibase/src/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/libs/uibase/src/pch.h b/libs/uibase/src/pch.h new file mode 100644 index 0000000..bd7e3bc --- /dev/null +++ b/libs/uibase/src/pch.h @@ -0,0 +1,139 @@ +#ifdef _MSC_VER +#pragma warning(disable : 4251) // neds to have dll-interface +#pragma warning(disable : 4355) // this used in initializer list +#pragma warning(disable : 4371) // layout may have changed +#pragma warning(disable : 4514) // unreferenced inline function removed +#pragma warning(disable : 4571) // catch semantics changed +#pragma warning(disable : 4619) // no warning X +#pragma warning(disable : 4623) // default constructor deleted +#pragma warning(disable : 4625) // copy constructor deleted +#pragma warning(disable : 4626) // copy assignment operator deleted +#pragma warning(disable : 4710) // function not inlined +#pragma warning(disable : 4820) // padding +#pragma warning(disable : 4866) // left-to-right evaluation order +#pragma warning(disable : 4868) // left-to-right evaluation order +#pragma warning(disable : 5026) // move constructor deleted +#pragma warning(disable : 5027) // move assignment operator deleted +#pragma warning(disable : 5045) // spectre mitigation + +#pragma warning(push, 3) +#pragma warning(disable : 4242) // uint -> const quint8 +#pragma warning(disable : 4365) // signed/unsigned mismatch +#pragma warning(disable : 4668) // preprocessor macro used but not defined +#pragma warning(disable : 4774) // bad format string +#pragma warning(disable : 4946) // reinterpret_cast used between related classes +#pragma warning(disable : 4800) // implicit conversion +#pragma warning(disable : 5219) // implicit int -> float conversion +#pragma warning( \ + disable : 5249) // named enumerators with values outside of bit field width +#endif // _MSC_VER + +#define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING 1 + +// std +#include <algorithm> +#include <cstdint> +#include <cwctype> +#include <filesystem> +#include <functional> +#include <iostream> +#include <list> +#include <map> +#include <memory> +#include <set> +#include <sstream> +#include <stdlib.h> +#include <string> +#include <typeindex> +#include <unordered_map> +#include <utility> +#include <vector> +#include <wchar.h> + +// windows +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_MEAN_AND_LEAN +#define WIN32_MEAN_AND_LEAN +#endif +#include <ShlObj.h> +#include <Windows.h> +#include <shobjidl.h> +#endif // _WIN32 + +// Qt +#include <QAbstractButton> +#include <QAction> +#include <QApplication> +#include <QBitmap> +#include <QBuffer> +#include <QCommandLinkButton> +#include <QCoreApplication> +#include <QCryptographicHash> +#include <QDateTime> +#include <QDialog> +#include <QDialogButtonBox> +#include <QDir> +#include <QDropEvent> +#include <QFile> +#include <QFileInfo> +#include <QFlags> +#include <QGraphicsObject> +#include <QIcon> +#include <QImage> +#include <QLabel> +#include <QLineEdit> +#include <QList> +#include <QMainWindow> +#include <QMenuBar> +#include <QMessageBox> +#include <QMetaEnum> +#include <QMetaMethod> +#include <QMetaObject> +#include <QMetaType> +#include <QMouseEvent> +#include <QMutex> +#include <QMutexLocker> +#include <QNetworkReply> +#include <QObject> +#include <QPlainTextEdit> +#include <QPushButton> +#include <QQmlContext> +#include <QQmlEngine> +#include <QQuickItem> +#include <QQuickWidget> +#include <QRect> +#include <QRegularExpression> +#include <QResizeEvent> +#include <QScreen> +#include <QSettings> +#include <QShortcutEvent> +#include <QShowEvent> +#include <QString> +#include <QStyle> +#include <QTabWidget> +#include <QTableWidget> +#include <QTemporaryFile> +#include <QTextEdit> +#include <QTextStream> +#include <QTime> +#include <QTimer> +#include <QToolBar> +#include <QToolButton> +#include <QTreeWidget> +#include <QUrl> +#include <QUrlQuery> +#include <QVBoxLayout> +#include <QVariant> +#include <QVariantMap> +#include <QVersionNumber> +#include <QWidget> +#include <QtDebug> +#include <qmetaobject.h> + +#undef _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING +#ifdef _MSC_VER +#pragma warning(pop) +#endif diff --git a/libs/uibase/src/pluginrequirements.cpp b/libs/uibase/src/pluginrequirements.cpp new file mode 100644 index 0000000..a2a11ef --- /dev/null +++ b/libs/uibase/src/pluginrequirements.cpp @@ -0,0 +1,138 @@ +#include <uibase/pluginrequirements.h> + +#include <QCoreApplication> + +#include <uibase/imoinfo.h> +#include <uibase/iplugindiagnose.h> +#include <uibase/iplugingame.h> + +using namespace MOBase; + +// Plugin and Game dependencies + +PluginDependencyRequirement::PluginDependencyRequirement(QStringList const& pluginNames) + : m_PluginNames(pluginNames) +{} + +std::optional<IPluginRequirement::Problem> +PluginDependencyRequirement::check(IOrganizer* o) const +{ + for (auto const& pluginName : m_PluginNames) { + if (o->isPluginEnabled(pluginName)) { + return {}; + } + } + return Problem(message()); +} + +QString PluginDependencyRequirement::message() const +{ + if (m_PluginNames.size() > 1) { + return QObject::tr("One of the following plugins must be enabled: %1.") + .arg(m_PluginNames.join(", ")); + } else { + return QObject::tr("This plugin can only be enabled if the '%1' plugin is " + "installed and enabled.") + .arg(m_PluginNames[0]); + } +} + +GameDependencyRequirement::GameDependencyRequirement(QStringList const& gameNames) + : m_GameNames(gameNames) +{} + +std::optional<IPluginRequirement::Problem> +GameDependencyRequirement::check(IOrganizer* o) const +{ + auto* game = o->managedGame(); + if (!game) { + return Problem(message()); + } + + QString gameName = game->gameName(); + for (auto const& pluginName : m_GameNames) { + if (pluginName.compare(gameName, Qt::CaseInsensitive) == 0) { + return {}; + } + } + return Problem(message()); +} + +QString GameDependencyRequirement::message() const +{ + return QObject::tr("This plugin can only be enabled for the following game(s): %1.", + "", static_cast<int>(m_GameNames.size())) + .arg(m_GameNames.join(", ")); +} + +// Diagnose requirements + +DiagnoseRequirement::DiagnoseRequirement(const IPluginDiagnose* diagnose) + : m_Diagnose(diagnose) +{} + +std::optional<IPluginRequirement::Problem> DiagnoseRequirement::check(IOrganizer*) const +{ + auto activeProblems = m_Diagnose->activeProblems(); + + if (activeProblems.empty()) { + return {}; + } + + QStringList shortDescriptions, longDescriptions; + for (auto i : activeProblems) { + shortDescriptions.append(m_Diagnose->shortDescription(i)); + longDescriptions.append(m_Diagnose->fullDescription(i)); + } + + return Problem(shortDescriptions.join("\n"), longDescriptions.join("\n")); +} + +// Basic requirements +class BasicPluginRequirement : public IPluginRequirement +{ +public: + BasicPluginRequirement(std::function<bool(IOrganizer*)> const& checker, + QString const description) + : m_Checker(checker), m_Description(description) + {} + + std::optional<Problem> check(IOrganizer* o) const + { + if (m_Checker(o)) { + return {}; + } + return Problem(m_Description); + } + +private: + std::function<bool(IOrganizer*)> m_Checker; + QString m_Description; +}; + +// Factory + +std::shared_ptr<const IPluginRequirement> +PluginRequirementFactory::pluginDependency(QStringList const& pluginNames) +{ + return std::make_shared<PluginDependencyRequirement>(pluginNames); +} + +std::shared_ptr<const IPluginRequirement> +PluginRequirementFactory::gameDependency(QStringList const& pluginGameNames) +{ + return std::make_shared<GameDependencyRequirement>(pluginGameNames); +} + +std::shared_ptr<const IPluginRequirement> +PluginRequirementFactory::diagnose(const IPluginDiagnose* diagnose) +{ + return std::make_shared<DiagnoseRequirement>(diagnose); +} + +std::shared_ptr<const IPluginRequirement> +PluginRequirementFactory::basic(std::function<bool(IOrganizer*)> const& checker, + QString const description) +{ + return std::make_shared<BasicPluginRequirement>(checker, description); +} diff --git a/libs/uibase/src/questionboxmemory.cpp b/libs/uibase/src/questionboxmemory.cpp new file mode 100644 index 0000000..586e52e --- /dev/null +++ b/libs/uibase/src/questionboxmemory.cpp @@ -0,0 +1,200 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include <uibase/questionboxmemory.h> +#include <uibase/log.h> +#include "ui_questionboxmemory.h" + +#include <QApplication> +#include <QIcon> +#include <QMutex> +#include <QMutexLocker> +#include <QPushButton> +#include <QSettings> +#include <QStyle> + +namespace MOBase +{ + +static QMutex g_mutex; +static QuestionBoxMemory::GetButton g_get; +static QuestionBoxMemory::SetWindowButton g_setWindow; +static QuestionBoxMemory::SetFileButton g_setFile; + +QuestionBoxMemory::QuestionBoxMemory(QWidget* parent, const QString& title, + const QString& text, QString const* filename, + const QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) + : QDialog(parent), ui(new Ui::QuestionBoxMemory), m_Button(QDialogButtonBox::Cancel) +{ + ui->setupUi(this); + + setWindowFlag(Qt::WindowType::WindowContextHelpButtonHint, false); + setWindowTitle(title); + + QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); + ui->iconLabel->setPixmap(icon.pixmap(128)); + ui->messageLabel->setText(text); + + if (filename == nullptr) { + // delete the 2nd check box + QCheckBox* box = ui->rememberForCheckBox; + box->parentWidget()->layout()->removeWidget(box); + delete box; + } else { + ui->rememberForCheckBox->setText(ui->rememberForCheckBox->text().arg(*filename)); + } + + ui->buttonBox->setStandardButtons(buttons); + + if (defaultButton != QDialogButtonBox::NoButton) { + ui->buttonBox->button(defaultButton)->setDefault(true); + } + + connect(ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), this, + SLOT(buttonClicked(QAbstractButton*))); +} + +QuestionBoxMemory::~QuestionBoxMemory() = default; + +void QuestionBoxMemory::setCallbacks(GetButton get, SetWindowButton setWindow, + SetFileButton setFile) +{ + QMutexLocker locker(&g_mutex); + + g_get = get; + g_setWindow = setWindow; + g_setFile = setFile; +} + +void QuestionBoxMemory::buttonClicked(QAbstractButton* button) +{ + m_Button = ui->buttonBox->standardButton(button); +} + +QDialogButtonBox::StandardButton +QuestionBoxMemory::query(QWidget* parent, const QString& windowName, + const QString& title, const QString& text, + QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) +{ + return queryImpl(parent, windowName, nullptr, title, text, buttons, defaultButton); +} + +QDialogButtonBox::StandardButton +QuestionBoxMemory::query(QWidget* parent, const QString& windowName, + const QString& fileName, const QString& title, + const QString& text, QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) +{ + return queryImpl(parent, windowName, &fileName, title, text, buttons, defaultButton); +} + +QDialogButtonBox::StandardButton +QuestionBoxMemory::queryImpl(QWidget* parent, const QString& windowName, + const QString* fileName, const QString& title, + const QString& text, + QDialogButtonBox::StandardButtons buttons, + QDialogButtonBox::StandardButton defaultButton) +{ + QMutexLocker locker(&g_mutex); + + const auto button = getMemory(windowName, (fileName ? *fileName : "")); + if (button != NoButton) { + log::debug("{}: not asking because user always wants response {}", + windowName + (fileName ? QString("/") + *fileName : ""), + buttonToString(button)); + + return button; + } + + QuestionBoxMemory dialog(parent, title, text, fileName, buttons, defaultButton); + dialog.exec(); + + if (dialog.m_Button != QDialogButtonBox::Cancel) { + if (dialog.ui->rememberCheckBox->isChecked()) { + setWindowMemory(windowName, dialog.m_Button); + } + + if (fileName != nullptr && dialog.ui->rememberForCheckBox->isChecked()) { + setFileMemory(windowName, *fileName, dialog.m_Button); + } + } + + return dialog.m_Button; +} + +void QuestionBoxMemory::setWindowMemory(const QString& windowName, Button b) +{ + log::debug("remembering choice {} for window {}", buttonToString(b), windowName); + + g_setWindow(windowName, b); +} + +void QuestionBoxMemory::setFileMemory(const QString& windowName, + const QString& filename, Button b) +{ + log::debug("remembering choice {} for file {}", buttonToString(b), + windowName + "/" + filename); + + g_setFile(windowName, filename, b); +} + +QuestionBoxMemory::Button QuestionBoxMemory::getMemory(const QString& windowName, + const QString& filename) +{ + return g_get(windowName, filename); +} + +QString QuestionBoxMemory::buttonToString(Button b) +{ + using BB = QDialogButtonBox; + + static const std::map<Button, QString> map = { + {BB::NoButton, "none"}, + {BB::Ok, "ok"}, + {BB::Save, "save"}, + {BB::SaveAll, "saveall"}, + {BB::Open, "open"}, + {BB::Yes, "yes"}, + {BB::YesToAll, "yestoall"}, + {BB::No, "no"}, + {BB::NoToAll, "notoall"}, + {BB::Abort, "abort"}, + {BB::Retry, "retry"}, + {BB::Ignore, "ignore"}, + {BB::Close, "close"}, + {BB::Cancel, "cancel"}, + {BB::Discard, "discard"}, + {BB::Help, "help"}, + {BB::Apply, "apply"}, + {BB::Reset, "reset"}, + {BB::RestoreDefaults, "restoredefaults"}}; + + auto itor = map.find(b); + + if (itor == map.end()) { + return QString("0x%1").arg(static_cast<int>(b), 0, 16); + } else { + return QString("'%1' (0x%2)").arg(itor->second).arg(static_cast<int>(b), 0, 16); + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/questionboxmemory.ui b/libs/uibase/src/questionboxmemory.ui new file mode 100644 index 0000000..8237571 --- /dev/null +++ b/libs/uibase/src/questionboxmemory.ui @@ -0,0 +1,171 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QuestionBoxMemory</class> + <widget class="QDialog" name="QuestionBoxMemory"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>374</width> + <height>130</height> + </rect> + </property> + <property name="windowTitle"> + <string notr="true">Placeholder</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout" stretch="1,0"> + <property name="spacing"> + <number>1</number> + </property> + <property name="leftMargin"> + <number>2</number> + </property> + <property name="topMargin"> + <number>2</number> + </property> + <property name="rightMargin"> + <number>2</number> + </property> + <property name="bottomMargin"> + <number>2</number> + </property> + <item> + <widget class="QFrame" name="frame"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0"> + <item> + <widget class="QLabel" name="iconLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>36</width> + <height>36</height> + </size> + </property> + <property name="autoFillBackground"> + <bool>false</bool> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="messageLabel"> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>7</number> + </property> + <property name="topMargin"> + <number>7</number> + </property> + <property name="rightMargin"> + <number>7</number> + </property> + <property name="bottomMargin"> + <number>7</number> + </property> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QCheckBox" name="rememberCheckBox"> + <property name="text"> + <string>Remember selection</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="rememberForCheckBox"> + <property name="text"> + <string>Remember selection only for %1</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::No|QDialogButtonBox::Yes</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QuestionBoxMemory</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QuestionBoxMemory</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/libs/uibase/src/registry.cpp b/libs/uibase/src/registry.cpp new file mode 100644 index 0000000..0b6834f --- /dev/null +++ b/libs/uibase/src/registry.cpp @@ -0,0 +1,189 @@ +/* +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <uibase/registry.h> +#include <uibase/log.h> +#include <uibase/report.h> +#include <QApplication> +#include <QFile> +#include <QFileInfo> +#include <QList> +#include <QMessageBox> +#include <QString> +#include <QTextStream> + +namespace MOBase +{ + +// Line-by-line INI writer that preserves the file format. +// Unlike QSettings::IniFormat, this does NOT interpret backslashes as +// line continuations, does NOT URL-encode spaces in key names, and does +// NOT reorder keys. It only modifies the target key=value pair and +// leaves everything else untouched. +static bool writeIniValueDirect(const QString& section, const QString& key, + const QString& value, const QString& fileName) +{ + QStringList lines; + bool fileExists = QFileInfo::exists(fileName); + + if (fileExists) { + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return false; + } + QTextStream in(&file); + while (!in.atEnd()) { + lines.append(in.readLine()); + } + file.close(); + } + + // Find the target section and key + QString sectionHeader = "[" + section + "]"; + int sectionStart = -1; + int sectionEnd = lines.size(); // end of file if section is last + int keyLine = -1; + + for (int i = 0; i < lines.size(); ++i) { + QString trimmed = lines[i].trimmed(); + if (trimmed.compare(sectionHeader, Qt::CaseInsensitive) == 0) { + sectionStart = i; + // Find end of this section (next section header or EOF) + for (int j = i + 1; j < lines.size(); ++j) { + QString t = lines[j].trimmed(); + if (t.startsWith('[') && t.endsWith(']')) { + sectionEnd = j; + break; + } + } + break; + } + } + + if (sectionStart >= 0) { + // Section found, look for the key within it + for (int i = sectionStart + 1; i < sectionEnd; ++i) { + QString trimmed = lines[i].trimmed(); + // Skip comments and empty lines + if (trimmed.isEmpty() || trimmed.startsWith(';') || trimmed.startsWith('#')) { + continue; + } + int eqPos = trimmed.indexOf('='); + if (eqPos > 0) { + QString existingKey = trimmed.left(eqPos).trimmed(); + if (existingKey.compare(key, Qt::CaseInsensitive) == 0) { + keyLine = i; + break; + } + } + } + + if (keyLine >= 0) { + // Key found, replace the line preserving indentation + QString original = lines[keyLine]; + int eqPos = original.indexOf('='); + // Preserve everything up to and including '=' + lines[keyLine] = original.left(eqPos + 1) + value; + } else { + // Key not found in section, insert it after the section header + lines.insert(sectionStart + 1, key + "=" + value); + } + } else { + // Section not found, append it + if (!lines.isEmpty() && !lines.last().trimmed().isEmpty()) { + lines.append(""); // blank line before new section + } + lines.append(sectionHeader); + lines.append(key + "=" + value); + } + + // Write back + QFile outFile(fileName); + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + return false; + } + QTextStream out(&outFile); + for (int i = 0; i < lines.size(); ++i) { + out << lines[i]; + if (i < lines.size() - 1) { + out << '\n'; + } + } + // Preserve trailing newline if original had one, or add one + out << '\n'; + outFile.close(); + + return true; +} + +bool WriteRegistryValue(const QString& appName, const QString& keyName, + const QString& value, const QString& fileName) +{ + if (writeIniValueDirect(appName, keyName, value, fileName)) { + return true; + } + + // Write failed, check if the file is read-only + QFileInfo fileInfo(fileName); + + QMessageBox::StandardButton result = + MOBase::TaskDialog(qApp->activeModalWidget(), + QObject::tr("INI file is read-only")) + .main(QObject::tr("INI file is read-only")) + .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" " + "which is currently set to read-only.") + .arg(fileInfo.fileName())) + .icon(QMessageBox::Warning) + .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes}) + .button({QObject::tr("Allow the write once"), + QObject::tr("The file will be set to read-only again."), + QMessageBox::Ignore}) + .button({QObject::tr("Skip this file"), QMessageBox::No}) + .remember("clearReadOnly", fileInfo.fileName()) + .exec(); + + if (result & (QMessageBox::Yes | QMessageBox::Ignore)) { + // Make the file writable + QFile file(fileName); + file.setPermissions(file.permissions() | QFile::WriteUser | QFile::WriteOwner); + + bool ok = writeIniValueDirect(appName, keyName, value, fileName); + + if (result == QMessageBox::Ignore) { + // Set back to read-only + file.setPermissions(file.permissions() & ~(QFile::WriteUser | QFile::WriteOwner)); + } + + return ok; + } + + return false; +} + +#ifdef _WIN32 +bool WriteRegistryValue(const wchar_t* appName, const wchar_t* keyName, + const wchar_t* value, const wchar_t* fileName) +{ + return WriteRegistryValue( + QString::fromWCharArray(appName), + QString::fromWCharArray(keyName), + QString::fromWCharArray(value), + QString::fromWCharArray(fileName)); +} +#endif + +} // namespace MOBase diff --git a/libs/uibase/src/report.cpp b/libs/uibase/src/report.cpp new file mode 100644 index 0000000..1c47d95 --- /dev/null +++ b/libs/uibase/src/report.cpp @@ -0,0 +1,453 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include <uibase/report.h> +#include <uibase/expanderwidget.h> +#include <uibase/log.h> +#include <uibase/questionboxmemory.h> +#include "ui_taskdialog.h" +#include <uibase/utility.h> +#include <QComboBox> +#include <QMainWindow> +#include <QRadioButton> +#include <QThread> + +namespace MOBase +{ + +QWidget* topLevelWindow() +{ + if (!qApp) { + return nullptr; + } + + for (QWidget* w : qApp->topLevelWidgets()) { + if (dynamic_cast<QMainWindow*>(w)) { + return w; + } + } + + return nullptr; +} + +void criticalOnTop(const QString& message) +{ + QMessageBox mb(QMessageBox::Critical, "Mod Organizer", message); + + mb.show(); + mb.activateWindow(); + mb.raise(); + mb.exec(); +} + +void reportError(const QString& message) +{ + log::error("{}", message); + + // QMessageBox must only be created on the main (GUI) thread. + // If we're on a background thread, just log — showing a dialog would crash. + if (QThread::currentThread() != qApp->thread()) { + return; + } + + if (QApplication::topLevelWidgets().count() != 0) { + if (auto* mw = topLevelWindow()) { + QMessageBox::warning(mw, QObject::tr("Error"), message, QMessageBox::Ok); + } else { + criticalOnTop(message); + } + } else { + // No top-level widget available, show a standalone critical dialog + criticalOnTop(message); + } +} + +TaskDialogButton::TaskDialogButton(QString t, QString d, QMessageBox::StandardButton b) + : text(std::move(t)), description(std::move(d)), button(b) +{} + +TaskDialogButton::TaskDialogButton(QString t, QMessageBox::StandardButton b) + : TaskDialogButton(std::move(t), {}, b) +{} + +TaskDialog::TaskDialog(QWidget* parent, QString title) + : m_dialog(new QDialog(parent)), ui(new Ui::TaskDialog), m_title(std::move(title)), + m_icon(QMessageBox::NoIcon), m_result(QMessageBox::Cancel), m_width(-1), + m_rememberCheck(nullptr), m_rememberCombo(nullptr) +{ + ui->setupUi(m_dialog.get()); +} + +TaskDialog::~TaskDialog() = default; + +TaskDialog& TaskDialog::title(const QString& s) +{ + m_title = s; + return *this; +} + +TaskDialog& TaskDialog::main(const QString& s) +{ + m_main = s; + return *this; +} + +TaskDialog& TaskDialog::content(const QString& s) +{ + m_content = s; + return *this; +} + +TaskDialog& TaskDialog::details(const QString& s) +{ + m_details = s; + return *this; +} + +TaskDialog& TaskDialog::icon(QMessageBox::Icon i) +{ + m_icon = i; + return *this; +} + +TaskDialog& TaskDialog::button(TaskDialogButton b) +{ + m_buttons.emplace_back(std::move(b)); + return *this; +} + +TaskDialog& TaskDialog::remember(const QString& action, const QString& file) +{ + m_rememberAction = action; + m_rememberFile = file; + return *this; +} + +void TaskDialog::addContent(QWidget* w) +{ + auto* ly = static_cast<QVBoxLayout*>(ui->contentPanel->layout()); + + // add before spacer + ly->insertWidget(ly->count() - 1, w); +} + +void TaskDialog::setWidth(int w) +{ + m_width = w; +} + +QMessageBox::StandardButton TaskDialog::exec() +{ + const auto b = checkMemory(); + if (b != QMessageBox::NoButton) { + return b; + } + + setDialog(); + setWidgets(); + setChoices(); + setButtons(); + setDetails(); + + if (m_width >= 0) { + ui->topPanel->setMinimumWidth(m_width); + } else { + ui->topPanel->setMinimumWidth(400); + } + + m_dialog->adjustSize(); + + if (!m_dialog->parent()) { + // no parent, make sure it's shown on top + m_dialog->show(); + m_dialog->activateWindow(); + m_dialog->raise(); + } + + if (m_dialog->exec() != QDialog::Accepted) { + return QMessageBox::Cancel; + } + + rememberChoice(); + return m_result; +} + +QMessageBox::StandardButton TaskDialog::checkMemory() const +{ + if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) { + return QMessageBox::NoButton; + } + + const auto b = QuestionBoxMemory::getMemory(m_rememberAction, m_rememberFile); + + const auto logName = m_rememberAction + + (m_rememberFile.isEmpty() ? "" : QString("/") + m_rememberFile); + + if (b == QDialogButtonBox::NoButton) { + log::debug("{}: asking because the user has not set a choice before", logName); + } else { + log::debug("{}: not asking because user always wants response {}", logName, + QuestionBoxMemory::buttonToString(b)); + } + + return static_cast<QMessageBox::StandardButton>(b); +} + +void TaskDialog::rememberChoice() +{ + if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) { + // nothing + return; + } + + const auto b = static_cast<QuestionBoxMemory::Button>(m_result); + + if (m_rememberCheck) { + // action only + if (m_rememberCheck->isChecked()) { + QuestionBoxMemory::setWindowMemory(m_rememberAction, b); + } + } else if (m_rememberCombo) { + Q_ASSERT(m_rememberCombo->count() == 3); + + if (m_rememberCombo->currentIndex() == 1) { + // remember action + QuestionBoxMemory::setWindowMemory(m_rememberAction, b); + } else if (m_rememberCombo->currentIndex() == 2) { + // remember file + QuestionBoxMemory::setFileMemory(m_rememberAction, m_rememberFile, b); + } + } +} + +void TaskDialog::setButtons() +{ + if (m_buttons.empty()) { + setStandardButtons(); + } else { + setCommandButtons(); + } + + m_expander.reset(new ExpanderWidget(ui->detailsExpander, ui->detailsWidget)); +} + +void TaskDialog::setStandardButtons() +{ + ui->standardButtonsPanel->show(); + ui->commandButtonsPanel->hide(); + + for (auto* b : ui->standardButtons->buttons()) { + ui->standardButtons->removeButton(b); + } + + ui->standardButtons->addButton(QDialogButtonBox::Ok); + + QObject::connect(ui->standardButtons, &QDialogButtonBox::clicked, [&](auto* b) { + m_result = static_cast<QMessageBox::StandardButton>( + ui->standardButtons->standardButton(b)); + + m_dialog->accept(); + }); +} + +void TaskDialog::setCommandButtons() +{ + ui->standardButtonsPanel->hide(); + ui->commandButtonsPanel->show(); + + deleteChildWidgets(ui->commandButtons); + + for (auto&& b : m_buttons) { + auto* cb = new QCommandLinkButton(b.text, b.description); + + QObject::connect(cb, &QAbstractButton::clicked, [&] { + m_result = b.button; + m_dialog->accept(); + }); + + ui->commandButtons->layout()->addWidget(cb); + } +} + +void TaskDialog::setDetails() +{ + if (m_details.isEmpty()) { + ui->detailsExpander->setEnabled(false); + return; + } + + ui->details->setPlainText(m_details); + + setVisibleLines(ui->details, 10); + setFontPercent(ui->details, 0.9); + + const QColor bg = detailsColor(); + + ui->detailsPanel->setStyleSheet(QString("background-color: rgb(%1, %2, %3)") + .arg(bg.redF() * 255) + .arg(bg.greenF() * 255) + .arg(bg.blueF() * 255)); +} + +void TaskDialog::setDialog() +{ +#ifdef _WIN32 + m_dialog->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint); +#else + m_dialog->setWindowFlags(Qt::Dialog); +#endif + m_dialog->layout()->setSizeConstraint(QLayout::SetFixedSize); + + m_dialog->setWindowTitle(m_title); +} + +void TaskDialog::setWidgets() +{ + ui->main->setText(m_main); + setFontPercent(ui->main, 1.5); + + ui->content->setText(m_content); + ui->content->setVisible(!m_content.isEmpty()); + + auto icon = standardIcon(m_icon); + + if (icon.isNull()) { + ui->iconPanel->hide(); + } else { + ui->iconPanel->show(); + ui->icon->setPixmap(std::move(icon)); + } +} + +void TaskDialog::setChoices() +{ + if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) { + ui->rememberPanel->hide(); + return; + } + + ui->rememberPanel->show(); + deleteChildWidgets(ui->rememberPanel); + + const auto tooltip = QObject::tr( + "You can reset these choices by clicking \"Reset Dialog Choices\" in the " + "General tab of the Settings"); + + if (!m_rememberAction.isEmpty() && !m_rememberFile.isEmpty()) { + // both + m_rememberCombo = new QComboBox; + m_rememberCombo->setToolTip(tooltip); + + m_rememberCombo->addItem(QObject::tr("Always ask")); + m_rememberCombo->addItem(QObject::tr("Remember my choice")); + m_rememberCombo->addItem( + QObject::tr("Remember my choice for %1").arg(m_rememberFile)); + + ui->rememberPanel->layout()->setAlignment(Qt::AlignLeft); + ui->rememberPanel->layout()->addWidget(m_rememberCombo); + } else if (!m_rememberAction.isEmpty() || !m_rememberFile.isEmpty()) { + // either + m_rememberCheck = new QCheckBox(QObject::tr("Remember my choice")); + m_rememberCheck->setToolTip(tooltip); + ui->rememberPanel->layout()->addWidget(m_rememberCheck); + } +} + +QColor TaskDialog::detailsColor() const +{ + auto b = std::make_unique<QPushButton>(); + m_dialog->style()->polish(b.get()); + return b->palette().color(b->backgroundRole()); +} + +QPixmap TaskDialog::standardIcon(QMessageBox::Icon icon) const +{ + QStyle* s = m_dialog->style(); + QIcon i; + + switch (icon) { + case QMessageBox::Information: + i = s->standardIcon(QStyle::SP_MessageBoxInformation, 0, m_dialog.get()); + break; + case QMessageBox::Warning: + i = s->standardIcon(QStyle::SP_MessageBoxWarning, 0, m_dialog.get()); + break; + case QMessageBox::Critical: + i = s->standardIcon(QStyle::SP_MessageBoxCritical, 0, m_dialog.get()); + break; + case QMessageBox::Question: + i = s->standardIcon(QStyle::SP_MessageBoxQuestion, 0, m_dialog.get()); + break; + case QMessageBox::NoIcon: + [[fallthrough]]; + default: + break; + } + + if (i.isNull()) { + return {}; + } + + QWindow* window = m_dialog->windowHandle(); + if (!window) { + if (const QWidget* nativeParent = m_dialog->nativeParentWidget()) + window = nativeParent->windowHandle(); + } + + const int iconSize = s->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, m_dialog.get()); + + return i.pixmap(QSize(iconSize, iconSize)); +} + +void TaskDialog::setVisibleLines(QPlainTextEdit* w, int lines) +{ + QTextDocument* d = w->document(); + QFontMetrics fm(d->defaultFont()); + + const QMargins margins = ui->details->contentsMargins(); + + double height = 0; + + // lines + height += fm.lineSpacing() * lines; + + // top and bottom margins for document and frame + height += (d->documentMargin() + ui->details->frameWidth()) * 2; + + // widget margins + height += margins.top() + margins.bottom(); + + w->setMaximumHeight(static_cast<int>(std::round(height))); +} + +void TaskDialog::setFontPercent(QWidget* w, double p) +{ + auto f = w->font(); + + if (f.pointSizeF() > 0) { + f.setPointSizeF(f.pointSizeF() * p); + } else if (f.pixelSize() > 0) { + f.setPixelSize(static_cast<int>(std::round(f.pixelSize() * p))); + } + + w->setFont(f); +} + +} // namespace MOBase diff --git a/libs/uibase/src/safewritefile.cpp b/libs/uibase/src/safewritefile.cpp new file mode 100644 index 0000000..fb0819f --- /dev/null +++ b/libs/uibase/src/safewritefile.cpp @@ -0,0 +1,84 @@ +/* +Copyright (C) 2014 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <uibase/safewritefile.h> +#include <uibase/log.h> +#include <QStorageInfo> +#include <QString> + +namespace MOBase +{ + +#ifdef _WIN32 + +SafeWriteFile::SafeWriteFile(const QString& fileName) : m_SaveFile(fileName) +{ + if (!m_SaveFile.open(QIODeviceBase::WriteOnly)) { + const auto av = + static_cast<double>(QStorageInfo(m_SaveFile.fileName()).bytesAvailable()); + + log::error( + "failed to create temporary file for '{}', error {} ('{}'), {:.3f}GB available", + m_SaveFile.fileName(), m_SaveFile.error(), m_SaveFile.errorString(), + (av / 1024 / 1024 / 1024)); + + throw Exception( + QObject::tr( + "Failed to save '%1', could not create a temporary file: %2 (error %3)") + .arg(m_SaveFile.fileName()) + .arg(m_SaveFile.errorString()) + .arg(m_SaveFile.error())); + } +} + +QSaveFile* SafeWriteFile::operator->() +{ + Q_ASSERT(m_SaveFile.isOpen()); + return &m_SaveFile; +} + +#else // Linux — use plain QFile (QSaveFile is unreliable across filesystems) + +SafeWriteFile::SafeWriteFile(const QString& fileName) : m_File(fileName) +{ + if (!m_File.open(QIODeviceBase::WriteOnly | QIODeviceBase::Truncate)) { + const auto av = + static_cast<double>(QStorageInfo(m_File.fileName()).bytesAvailable()); + + log::error("failed to open '{}' for writing, error {} ('{}'), {:.3f}GB available", + m_File.fileName(), m_File.error(), m_File.errorString(), + (av / 1024 / 1024 / 1024)); + + throw Exception( + QObject::tr("Failed to save '%1': %2 (error %3)") + .arg(m_File.fileName()) + .arg(m_File.errorString()) + .arg(m_File.error())); + } +} + +DirectWriteFile* SafeWriteFile::operator->() +{ + Q_ASSERT(m_File.isOpen()); + return &m_File; +} + +#endif + +} // namespace MOBase diff --git a/libs/uibase/src/scopeguard.cpp b/libs/uibase/src/scopeguard.cpp new file mode 100644 index 0000000..a671d1e --- /dev/null +++ b/libs/uibase/src/scopeguard.cpp @@ -0,0 +1,6 @@ +#include <uibase/scopeguard.h> + +namespace MOBase +{ + +} // namespace MOBase diff --git a/libs/uibase/src/sortabletreewidget.cpp b/libs/uibase/src/sortabletreewidget.cpp new file mode 100644 index 0000000..5cfbdd4 --- /dev/null +++ b/libs/uibase/src/sortabletreewidget.cpp @@ -0,0 +1,116 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2013 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include <uibase/sortabletreewidget.h> +#include <QDropEvent> + +namespace MOBase +{ + +SortableTreeWidget::SortableTreeWidget(QWidget* parent) + : QTreeWidget(parent), m_LocalMoveOnly(false) +{} + +void SortableTreeWidget::setLocalMoveOnly(bool localOnly) +{ + m_LocalMoveOnly = localOnly; +} + +void SortableTreeWidget::dropEvent(QDropEvent* event) +{ + // only react on internal drag&drop + if (event->source() == this) { + QTreeWidget::dropEvent(event); + } +} + +Qt::DropActions SortableTreeWidget::supportedDropActions() const +{ + // we do our own drop handling, this ensures dropMimeData is called. + return Qt::CopyAction; +} + +bool SortableTreeWidget::dropMimeData(QTreeWidgetItem* parent, int index, + const QMimeData*, Qt::DropAction) +{ + // TODO: ignores type of action set up in designer, always moves + return this->moveSelection(parent, index); +} + +bool SortableTreeWidget::moveSelection(QTreeWidgetItem* parent, int idx) +{ + QModelIndex parentIndex = indexFromItem(parent); + std::vector<QPersistentModelIndex> persistentIndices; + Q_FOREACH (const QModelIndex& index, selectedIndexes()) { + if (index == parentIndex) + return false; + if (m_LocalMoveOnly && (parentIndex != index.parent())) + return false; + if (index.column() != 0) + continue; + persistentIndices.push_back(index); + } + + int targetRow = -1; + if (itemsExpandable() || !parentIndex.isValid()) { + targetRow = model()->index(idx, 0, parentIndex).row(); + + if (targetRow < 0) { + targetRow = idx; + } + } else { + // if items aren't expandable, we can't be placing items on sublevels + targetRow = parentIndex.row(); + parentIndex = QModelIndex(); + } + + // remove items from the list, store in temporary location + QList<QTreeWidgetItem*> temp; + for (auto iter = persistentIndices.rbegin(); iter != persistentIndices.rend(); + ++iter) { + QTreeWidgetItem* item = itemFromIndex(*iter); + if (item == nullptr) + continue; + if ((item == nullptr) || (item->parent() == nullptr)) { + temp.append(takeTopLevelItem(iter->row())); + } else { + temp.append(item->parent()->takeChild(iter->row())); + } + } + + if (idx == -1) { + // append + if (parentIndex.isValid()) { + parent->addChildren(temp); + } else { + addTopLevelItems(temp); + } + } else { + if (parentIndex.isValid()) { + parent->insertChildren(std::min<int>(targetRow, parent->childCount()), temp); + } else { + insertTopLevelItems(std::min<int>(targetRow, topLevelItemCount()), temp); + } + } + emit itemsMoved(); + return true; +} + +} // namespace MOBase diff --git a/libs/uibase/src/steamutility.cpp b/libs/uibase/src/steamutility.cpp new file mode 100644 index 0000000..a9dd80a --- /dev/null +++ b/libs/uibase/src/steamutility.cpp @@ -0,0 +1,108 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2019 MO2 Contributors. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include <uibase/steamutility.h> + +#include <QDir> +#include <QList> +#include <QRegularExpression> +#include <QSettings> +#include <QString> +#include <QTextStream> + +namespace MOBase +{ + +// Lines that contains libraries are in the format: +// "1" "Path\to\library" +static const QRegularExpression + kSteamLibraryFilter("^\\s*\"(?<idx>[0-9]+)\"\\s*\"(?<path>.*)\""); + +QString findSteam() +{ +#ifdef _WIN32 + QSettings steamRegistry("Valve", "Steam"); + return steamRegistry.value("SteamPath").toString(); +#else + // On Linux, check common Steam locations + QString homePath = QDir::homePath(); + QStringList candidates = { + homePath + "/.steam/steam", + homePath + "/.local/share/Steam", + homePath + "/.steam/debian-installation" + }; + for (const auto& path : candidates) { + if (QDir(path).exists()) { + return path; + } + } + return ""; +#endif +} + +QString findSteamGame(const QString& appName, const QString& validFile) +{ + QStringList libraryFolders; // list of Steam libraries to search + QDir steamDir(findSteam()); // Steam installation directory + + // Can do nothing if Steam doesn't exist + if (!steamDir.exists()) + return ""; + + // The Steam install is always a valid library + libraryFolders << steamDir.absolutePath(); + + // Search libraryfolders.vdf for additional libraries + QFile libraryFoldersFile(steamDir.absoluteFilePath("steamapps/libraryfolders.vdf")); + if (libraryFoldersFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QTextStream in(&libraryFoldersFile); + while (!in.atEnd()) { + QString line = in.readLine(); + QRegularExpressionMatch match = kSteamLibraryFilter.match(line); + if (match.hasMatch()) { + QString folder = match.captured("path"); +#ifdef _WIN32 + folder.replace("/", "\\").replace("\\\\", "\\"); +#else + folder.replace("\\\\", "/").replace("\\", "/"); +#endif + libraryFolders << folder; + } + } + } + + // Search the Steam libraries for the game directory + for (auto library : libraryFolders) { + QDir libraryDir(library); + if (!libraryDir.cd("steamapps/common/" + appName)) + continue; + QString normalizedValidFile = validFile; + normalizedValidFile.replace("\\", "/"); + if (normalizedValidFile.startsWith('/')) { + normalizedValidFile.remove(0, 1); + } + if (normalizedValidFile.isEmpty() || libraryDir.exists(normalizedValidFile)) + return libraryDir.absolutePath(); + } + + return ""; +} + +} // namespace MOBase diff --git a/libs/uibase/src/strings.cpp b/libs/uibase/src/strings.cpp new file mode 100644 index 0000000..11087f4 --- /dev/null +++ b/libs/uibase/src/strings.cpp @@ -0,0 +1,46 @@ +#include <uibase/strings.h> + +#include <algorithm> +#include <locale> + +namespace MOBase +{ + +// this is strongly inspired from boost +class is_iequal +{ + std::locale m_loc; + +public: + is_iequal(const std::locale& loc = std::locale()) : m_loc{loc} {} + + template <typename T1, typename T2> + bool operator()(const T1& Arg1, const T2& Arg2) const + { + return std::toupper<T1>(Arg1, m_loc) == std::toupper<T2>(Arg2, m_loc); + } +}; + +bool iequals(std::string_view lhs, std::string_view rhs) +{ + return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), is_iequal()); +} + +void ireplace_all(std::string& input, std::string_view search, + std::string_view replace) noexcept +{ + const auto search_length = static_cast<std::string::difference_type>(search.size()); + const auto replace_length = replace.size(); + + std::size_t i = 0; + while (input.size() - i >= search_length) { + if (iequals(std::string_view(input).substr(i, search_length), search)) { + input.replace(i, search_length, replace); + i += replace_length; + } else { + ++i; + } + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/taskdialog.ui b/libs/uibase/src/taskdialog.ui new file mode 100644 index 0000000..727312d --- /dev/null +++ b/libs/uibase/src/taskdialog.ui @@ -0,0 +1,359 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TaskDialog</class> + <widget class="QDialog" name="TaskDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>436</width> + <height>316</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="topPanel" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1"> + <item> + <widget class="QWidget" name="iconPanel" native="true"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="leftMargin"> + <number>9</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>9</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="icon"> + <property name="text"> + <string>icon</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_3" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_10"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="mainPanel" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_11"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="main"> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="text"> + <string>dummy main text</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="mainSpacer" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>10</height> + </size> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="contentPanel" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="content"> + <property name="text"> + <string>dummy content text</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="buttonsSpacer" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>10</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout_7"/> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="commandButtonsPanel" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="commandButtons" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_9"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QCommandLinkButton" name="dummyButton"> + <property name="text"> + <string>dummy button</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="rememberPanel" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_12"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QCheckBox" name="dummyCheckbox"> + <property name="text"> + <string>dummy checkbox</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>0</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QWidget" name="standardButtonsPanel" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <item> + <widget class="QDialogButtonBox" name="standardButtons"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="detailsPanel" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="widget_7" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="topMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QToolButton" name="detailsExpander"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true">border: none; +text-align: left;</string> + </property> + <property name="text"> + <string>Details</string> + </property> + </widget> + </item> + <item> + <widget class="QWidget" name="detailsWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>9</number> + </property> + <item> + <widget class="QPlainTextEdit" name="details"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/libs/uibase/src/taskprogressmanager.cpp b/libs/uibase/src/taskprogressmanager.cpp new file mode 100644 index 0000000..3bcffb0 --- /dev/null +++ b/libs/uibase/src/taskprogressmanager.cpp @@ -0,0 +1,125 @@ +#include <uibase/taskprogressmanager.h> +#include <uibase/log.h> +#include <uibase/utility.h> +#include <QApplication> +#include <QMainWindow> +#include <QWidget> + +namespace MOBase +{ + +TaskProgressManager& TaskProgressManager::instance() +{ + static TaskProgressManager s_Instance; + return s_Instance; +} + +void TaskProgressManager::forgetMe(quint32 id) +{ + if (m_Taskbar == nullptr) { + return; + } + auto iter = m_Percentages.find(id); + if (iter != m_Percentages.end()) { + m_Percentages.erase(iter); + } + showProgress(); +} + +void TaskProgressManager::updateProgress(quint32 id, qint64 value, qint64 max) +{ + QMutexLocker lock(&m_Mutex); + if (m_Taskbar == nullptr) { + return; + } + + if (value == max) { + auto iter = m_Percentages.find(id); + if (iter != m_Percentages.end()) { + m_Percentages.erase(iter); + } + } else { + m_Percentages[id] = std::make_pair(QTime::currentTime(), (value * 100) / max); + } + + showProgress(); +} + +quint32 TaskProgressManager::getId() +{ + QMutexLocker lock(&m_Mutex); + return m_NextId++; +} + +void TaskProgressManager::showProgress() +{ +#ifdef _WIN32 + if (!m_Percentages.empty()) { + m_Taskbar->SetProgressState(m_WinId, TBPF_NORMAL); + + QTime now = QTime::currentTime(); + unsigned long long total = 0; + unsigned long long count = 0; + + for (auto iter = m_Percentages.begin(); iter != m_Percentages.end();) { + if (iter->second.first.secsTo(now) < 15) { + total += static_cast<unsigned long long>(iter->second.second); + ++iter; + ++count; + } else { + log::debug("no progress in 15 seconds ({})", iter->second.first.secsTo(now)); + iter = m_Percentages.erase(iter); + } + } + + m_Taskbar->SetProgressValue(m_WinId, total, count * 100); + } else { + m_Taskbar->SetProgressState(m_WinId, TBPF_NOPROGRESS); + } +#else + // On Linux, taskbar progress is not supported in this implementation. + // Could integrate with D-Bus com.canonical.Unity.LauncherEntry or similar. + (void)m_Percentages; +#endif +} + +bool TaskProgressManager::tryCreateTaskbar() +{ +#ifdef _WIN32 + // try to find our main window + for (QWidget* widget : QApplication::topLevelWidgets()) { + QMainWindow* mainWin = qobject_cast<QMainWindow*>(widget); + if (mainWin != nullptr) { + m_WinId = reinterpret_cast<HWND>(mainWin->winId()); + } + } + + HRESULT result = 0; + if (m_WinId != nullptr) { + result = CoCreateInstance(CLSID_TaskbarList, 0, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&m_Taskbar)); + if (result == S_OK) { + return true; + } + } + + m_Taskbar = nullptr; + + if (m_CreateTries-- > 0) { + QTimer::singleShot(1000, this, SLOT(tryCreateTaskbar())); + } else { + log::warn("failed to create taskbar connection"); + } + return false; +#else + // On Linux, no Windows taskbar API + m_Taskbar = nullptr; + return false; +#endif +} + +TaskProgressManager::TaskProgressManager() + : m_NextId(1), m_CreateTries(10), m_WinId(nullptr), m_Taskbar(nullptr) +{} + +} // namespace MOBase diff --git a/libs/uibase/src/textviewer.cpp b/libs/uibase/src/textviewer.cpp new file mode 100644 index 0000000..8c7ac0f --- /dev/null +++ b/libs/uibase/src/textviewer.cpp @@ -0,0 +1,278 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include <uibase/textviewer.h> +#include <uibase/finddialog.h> +#include <uibase/log.h> +#include <uibase/report.h> +#include "ui_textviewer.h" +#include <uibase/utility.h> +#include <QAction> +#include <QFile> +#include <QFileInfo> +#include <QMessageBox> +#include <QPushButton> +#include <QShortcutEvent> +#include <QTextEdit> +#include <QVBoxLayout> + +namespace MOBase +{ + +TextViewer::TextViewer(const QString& title, QWidget* parent) + : QDialog(parent), ui(new Ui::TextViewer), m_FindDialog(nullptr) +{ + ui->setupUi(this); + setWindowTitle(title); + m_EditorTabs = findChild<QTabWidget*>("editorTabs"); + connect(ui->showWhitespace, SIGNAL(stateChanged(int)), this, + SLOT(showWhitespaceChanged(int))); +} + +TextViewer::~TextViewer() +{ + delete ui; +} + +void TextViewer::closeEvent(QCloseEvent* event) +{ + if (!m_Modified.empty()) { + for (std::set<QTextEdit*>::iterator iter = m_Modified.begin(); + iter != m_Modified.end(); ++iter) { + QMessageBox::StandardButton res = QMessageBox::question( + this, tr("Save changes?"), + tr("Do you want to save changes to %1?").arg((*iter)->documentTitle()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Yes) { + saveFile(*iter); + } else if (res == QMessageBox::Cancel) { + event->ignore(); + break; + } + } + } +} + +void TextViewer::find() +{ + if (!m_FindDialog) { + m_FindDialog = new FindDialog(this); + connect(m_FindDialog, SIGNAL(findNext()), this, SLOT(findNext())); + connect(m_FindDialog, SIGNAL(patternChanged(QString)), this, + SLOT(patternChanged(QString))); + } + + m_FindDialog->show(); + m_FindDialog->raise(); + m_FindDialog->activateWindow(); +} + +void TextViewer::patternChanged(QString newPattern) +{ + m_FindPattern = newPattern; +} + +void TextViewer::findNext() +{ + if (m_FindPattern.length() == 0) { + return; + } + + QWidget* currentPage = m_EditorTabs->currentWidget(); + QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView"); + + if (editor->find(m_FindPattern)) { + // found text + return; + } else { + // reached the bottom and no text found, + // we wrap around once. + + // save current cursor + auto oldCursor = editor->textCursor(); + + editor->moveCursor(QTextCursor::Start); + + // search again from the top + if (editor->find(m_FindPattern)) { + // found something, keep new cursor position. + return; + } else { + // there are no matches in the document, + // restore previous cursor. + editor->setTextCursor(oldCursor); + } + } +} + +void TextViewer::showWhitespaceChanged(int state) +{ + for (int i = 0; i < m_EditorTabs->count(); ++i) { + QTextEdit* editor = m_EditorTabs->widget(i)->findChild<QTextEdit*>(); + if (editor != nullptr) { + auto document = editor->document(); + auto textOption = document->defaultTextOption(); + auto flags = textOption.flags(); + if (state == Qt::Unchecked) + flags = flags & (~QTextOption::ShowTabsAndSpaces); + else + flags = flags | QTextOption::ShowTabsAndSpaces; + textOption.setFlags(flags); + document->setDefaultTextOption(textOption); + editor->setDocument(document); + } + } +} + +bool TextViewer::eventFilter(QObject* object, QEvent* event) +{ + if (event->type() == QEvent::ShortcutOverride) { + QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->matches(QKeySequence::Find)) { + find(); + } else if (keyEvent->matches(QKeySequence::FindNext)) { + findNext(); + } + } + return QDialog::eventFilter(object, event); +} + +void TextViewer::setDescription(const QString& description) +{ + QLabel* descriptionLabel = findChild<QLabel*>("descriptionLabel"); + descriptionLabel->setText(description); +} + +void TextViewer::saveFile(const QTextEdit* editor) +{ + bool write = true; + QMessageBox::StandardButton buttonPressed = QMessageBox::Ignore; + QFile file(editor->documentTitle()); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + write = false; + QFileInfo fileInfo(file.fileName()); + buttonPressed = + MOBase::TaskDialog(qApp->activeModalWidget(), + QObject::tr("INI file is read-only")) + .main(QObject::tr("INI file is read-only")) + .content(QObject::tr("Mod Organizer is attempting to write to \"%1\" which " + "is currently set to read-only.") + .arg(fileInfo.fileName())) + .icon(QMessageBox::Warning) + .button({QObject::tr("Clear the read-only flag"), QMessageBox::Yes}) + .button({QObject::tr("Allow the write once"), + QObject::tr("The file will be set to read-only again."), + QMessageBox::Ignore}) + .button({QObject::tr("Skip this file"), QMessageBox::No}) + .remember("clearReadOnly", fileInfo.fileName()) + .exec(); + + if (buttonPressed & (QMessageBox::Yes | QMessageBox::Ignore)) { + file.setPermissions(file.permissions() | QFile::WriteUser); + } + + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + reportError(tr("failed to write to %1").arg(editor->documentTitle())); + } else { + write = true; + } + } + + if (write) { + file.write(editor->toPlainText().toUtf8().replace('\n', "\r\n")); + file.close(); + } + + if (buttonPressed == QMessageBox::Ignore) { + file.setPermissions(file.permissions() & ~(QFile::WriteUser)); + } +} + +void TextViewer::saveFile() +{ + QWidget* currentPage = m_EditorTabs->currentWidget(); + QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView"); + saveFile(editor); + + m_Modified.erase(editor); +} + +void TextViewer::modified() +{ + QWidget* currentPage = m_EditorTabs->currentWidget(); + QTextEdit* editor = currentPage->findChild<QTextEdit*>("editorView"); + + m_Modified.insert(editor); +} + +void TextViewer::addFile(const QString& fileName, bool writable) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + throw Exception(tr("file not found: %1").arg(fileName)); + } + QByteArray temp = file.readAll(); + + QWidget* page = new QWidget(); + QVBoxLayout* layout = new QVBoxLayout(page); + QTextEdit* editor = new QTextEdit(page); + QTextDocument* document = new QTextDocument(page); + if (ui->showWhitespace->isChecked()) { + QTextOption option; + option.setFlags(QTextOption::ShowTabsAndSpaces); + document->setDefaultTextOption(option); + } + editor->setDocument(document); + editor->setAcceptRichText(false); + editor->setPlainText(QString(temp)); + editor->setLineWrapMode(QTextEdit::NoWrap); + editor->setObjectName("editorView"); + editor->setDocumentTitle(fileName); + editor->installEventFilter(this); + editor->setReadOnly(!writable); + + // set text highlighting color in inactive window equal to text hightlighting color in + // active window + QPalette palette = editor->palette(); + palette.setColor(QPalette::Inactive, QPalette::Highlight, + palette.color(QPalette::Active, QPalette::Highlight)); + palette.setColor(QPalette::Inactive, QPalette::HighlightedText, + palette.color(QPalette::Active, QPalette::HighlightedText)); + editor->setPalette(palette); + + // add hotkeys for searching through the document + QAction* findAction = new QAction(QString("&Find"), editor); + findAction->setShortcut(QKeySequence::Find); + editor->addAction(findAction); + QAction* findNextAction = new QAction(QString("Find &Next"), editor); + findAction->setShortcut(QKeySequence::FindNext); + editor->addAction(findNextAction); + + layout->addWidget(editor); + if (writable) { + QPushButton* saveBtn = new QPushButton(tr("Save"), page); + layout->addWidget(saveBtn); + connect(saveBtn, SIGNAL(clicked()), this, SLOT(saveFile())); + connect(editor, SIGNAL(textChanged()), this, SLOT(modified())); + } + page->setLayout(layout); + m_EditorTabs->addTab(page, QFileInfo(fileName).fileName()); +} +} // namespace MOBase diff --git a/libs/uibase/src/textviewer.ui b/libs/uibase/src/textviewer.ui new file mode 100644 index 0000000..e2af30e --- /dev/null +++ b/libs/uibase/src/textviewer.ui @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TextViewer</class> + <widget class="QDialog" name="TextViewer"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>607</width> + <height>528</height> + </rect> + </property> + <property name="windowTitle"> + <string>Log Viewer</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLabel" name="descriptionLabel"> + <property name="text"> + <string>Placeholder</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="showWhitespace"> + <property name="text"> + <string>Show Whitespace</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTabWidget" name="editorTabs"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/libs/uibase/src/tutorabledialog.cpp b/libs/uibase/src/tutorabledialog.cpp new file mode 100644 index 0000000..61aad62 --- /dev/null +++ b/libs/uibase/src/tutorabledialog.cpp @@ -0,0 +1,40 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <uibase/tutorabledialog.h> + +namespace MOBase +{ + +TutorableDialog::TutorableDialog(const QString& name, QWidget* parent) + : QDialog(parent), m_Tutorial(this, name) +{} + +void TutorableDialog::showEvent(QShowEvent* event) +{ + m_Tutorial.registerControl(); + QDialog::showEvent(event); +} + +void TutorableDialog::resizeEvent(QResizeEvent* event) +{ + m_Tutorial.resize(event->size()); + QDialog::resizeEvent(event); +} +} // namespace MOBase diff --git a/libs/uibase/src/tutorialcontrol.cpp b/libs/uibase/src/tutorialcontrol.cpp new file mode 100644 index 0000000..fee19f4 --- /dev/null +++ b/libs/uibase/src/tutorialcontrol.cpp @@ -0,0 +1,384 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <uibase/tutorialcontrol.h> +#include <uibase/log.h> +#include <uibase/report.h> +#include <uibase/tutorialmanager.h> +#include <uibase/utility.h> + +#include <QAbstractButton> +#include <QAction> +#include <QBitmap> +#include <QCoreApplication> +#include <QDir> +#include <QFile> +#include <QGraphicsObject> +#include <QImage> +#include <QMenuBar> +#include <QMouseEvent> +#include <QQmlContext> +#include <QQmlEngine> +#include <QQuickItem> +#include <QTableWidget> +#include <QTimer> +#include <QToolBar> + +#include <QMetaEnum> +#include <QMetaMethod> +#include <QMetaObject> +#include <qmetaobject.h> + +namespace MOBase +{ + +TutorialControl::TutorialControl(const TutorialControl& reference) + : QObject(reference.parent()), m_TargetControl(reference.m_TargetControl), + m_Name(reference.m_Name), m_TutorialView(nullptr), + m_Manager(TutorialManager::instance()), m_ExpectedTab(0), + m_CurrentClickControl(nullptr) +{} + +TutorialControl::TutorialControl(QWidget* targetControl, const QString& name) + : QObject(nullptr), m_TargetControl(targetControl), m_Name(name), + m_TutorialView(nullptr), m_Manager(TutorialManager::instance()), m_ExpectedTab(0), + m_CurrentClickControl(nullptr) +{} + +TutorialControl::~TutorialControl() +{ + m_Manager.unregisterControl(m_Name); + finish(); +} + +void TutorialControl::registerControl() +{ + m_Manager.registerControl(m_Name, this); +} + +void TutorialControl::resize(const QSize& size) +{ + if (m_TutorialView != nullptr) { + m_TutorialView->resize(size.width(), size.height()); + } +} + +void TutorialControl::expose(const QString& widgetName, QObject* widget) +{ + m_ExposedObjects.push_back(std::make_pair(widgetName, widget)); +} + +static QString canonicalPath(const QString& path) +{ +#ifdef _WIN32 + std::unique_ptr<wchar_t[]> buffer(new wchar_t[32768]); + DWORD res = ::GetShortPathNameW((wchar_t*)path.utf16(), buffer.get(), 32768); + if (res == 0) { + return path; + } + res = ::GetLongPathNameW(buffer.get(), buffer.get(), 32768); + if (res == 0) { + return path; + } + return QString::fromWCharArray(buffer.get()); +#else + QFileInfo fi(path); + QString canonical = fi.canonicalFilePath(); + return canonical.isEmpty() ? path : canonical; +#endif +} + +void TutorialControl::startTutorial(const QString& tutorial) +{ + if (m_TutorialView == nullptr) { + m_TutorialView = new QQuickWidget(m_TargetControl); + m_TutorialView->setResizeMode(QQuickWidget::SizeRootObjectToView); + m_TutorialView->setAttribute(Qt::WA_TranslucentBackground); + m_TutorialView->setAttribute(Qt::WA_AlwaysStackOnTop); + m_TutorialView->setClearColor(Qt::transparent); + m_TutorialView->setStyleSheet("background: transparent"); + m_TutorialView->setObjectName("tutorialView"); + m_TutorialView->rootContext()->setContextProperty("manager", &m_Manager); + + QString qmlName = + canonicalPath(QCoreApplication::applicationDirPath() + "/tutorials") + + "/tutorials_" + m_Name.toLower() + ".qml"; + QUrl qmlSource = QUrl::fromLocalFile(qmlName); + + m_TutorialView->setSource(qmlSource); + m_TutorialView->resize(m_TargetControl->width(), m_TargetControl->height()); + m_TutorialView->rootContext()->setContextProperty("scriptName", tutorial); + m_TutorialView->rootContext()->setContextProperty("tutorialControl", this); + m_TutorialView->rootContext()->setContextProperty("applicationWindow", + m_TargetControl); + m_TutorialView->rootContext()->setContextProperty("organizer", + m_Manager.organizerCore()); + + for (std::vector<std::pair<QString, QObject*>>::const_iterator iter = + m_ExposedObjects.begin(); + iter != m_ExposedObjects.end(); ++iter) { + m_TutorialView->rootContext()->setContextProperty(iter->first, iter->second); + } + m_TutorialView->show(); + m_TutorialView->raise(); + if (!QMetaObject::invokeMethod(m_TutorialView->rootObject(), "init")) { + reportError(tr( + "Tutorial failed to start, please check \"mo_interface.log\" for details.")); + m_TutorialView->close(); + } + } +} + +void TutorialControl::lockUI(bool locked) +{ + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, !locked); + + QMetaObject::invokeMethod(m_TutorialView->rootObject(), "enableBackground", + Q_ARG(QVariant, QVariant(locked))); +} + +void TutorialControl::simulateClick(int x, int y) +{ + bool wasTransparent = m_TutorialView->testAttribute(Qt::WA_TransparentForMouseEvents); + if (!wasTransparent) { + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, true); + } + QWidget* hitControl = m_TargetControl->childAt(x, y); + QPoint globalPos = m_TargetControl->mapToGlobal(QPoint(x, y)); + QPoint hitPos = hitControl->mapFromGlobal(globalPos); + QMouseEvent* downEvent = new QMouseEvent( + QEvent::MouseButtonPress, QPointF(hitPos), QPointF(globalPos), + Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QMouseEvent* upEvent = + new QMouseEvent(QEvent::MouseButtonRelease, QPointF(hitPos), QPointF(globalPos), + Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + + qApp->postEvent(hitControl, (QEvent*)downEvent); + qApp->postEvent(hitControl, (QEvent*)upEvent); + + if (!wasTransparent) { + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, false); + } +} + +QWidget* TutorialControl::getChild(const QString& name) +{ + if (m_TargetControl != nullptr) { + return m_TargetControl->findChild<QWidget*>(name); + } else { + return nullptr; + } +} + +void TutorialControl::finish() +{ + if (m_TutorialView != nullptr) { + m_TutorialView->deleteLater(); + } + m_TutorialView = nullptr; + m_TutorialView = nullptr; +} + +QRect TutorialControl::getRect(const QString& widgetName) +{ + if (m_TargetControl != nullptr) { + QWidget* widget = m_TargetControl->findChild<QWidget*>(widgetName); + if (widget != nullptr) { + QRect res = widget->rect(); + QPoint pos = widget->mapTo(m_TargetControl, res.topLeft()); + res.moveTopLeft(pos); + return res; + } else { + log::error("{} not found", widgetName); + return QRect(); + } + } else { + return QRect(); + } +} + +QRect TutorialControl::getActionRect(const QString& widgetName) +{ + if (m_TargetControl != nullptr) { + QToolBar* toolBar = m_TargetControl->findChild<QToolBar*>("toolBar"); + foreach (QAction* action, toolBar->actions()) { + if (action->objectName() == widgetName) { + return toolBar->actionGeometry(action); + } + } + } + return QRect(); +} + +QRect TutorialControl::getMenuRect(const QString&) +{ + if (m_TargetControl != nullptr) { + QMenuBar* menuBar = m_TargetControl->findChild<QMenuBar*>("menuBar"); + return menuBar->geometry(); + } + return QRect(); +} + +void TutorialControl::nextTutorialStepProxy() +{ + + if (m_TutorialView != nullptr) { + QObject* background = m_TutorialView->rootObject(); + + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + + bool success = true; + for (QMetaObject::Connection connection : m_Connections) { + if (!disconnect(connection)) { + success = false; + } + } + m_Connections.clear(); + if (!success) { + log::error("failed to disconnect tutorial proxy"); + } + } else { + log::error("failed to proceed to next tutorial step"); + finish(); + } +} + +void TutorialControl::tabChangedProxy(int selected) +{ + if ((m_TutorialView != nullptr) && (selected == m_ExpectedTab)) { + QObject* background = m_TutorialView->rootObject(); + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + bool success = true; + for (QMetaObject::Connection connection : m_Connections) { + if (!disconnect(connection)) { + success = false; + } + } + m_Connections.clear(); + if (!success) { + log::error("failed to disconnect tab-changed proxy"); + } + } +} + +bool TutorialControl::waitForAction(const QString& actionName) +{ + if (m_TargetControl != nullptr) { + QAction* action = m_TargetControl->findChild<QAction*>(actionName); + if (action == nullptr) { + log::error("no action \"{}\" in control \"{}\"", actionName, m_Name); + return false; + } + if (action->isEnabled()) { + if (action->menu() != nullptr) { + m_Connections.append(connect(action->menu(), SIGNAL(aboutToShow()), this, + SLOT(nextTutorialStepProxy()))); + } else { + m_Connections.append( + connect(action, SIGNAL(triggered()), this, SLOT(nextTutorialStepProxy()))); + } + lockUI(false); + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool TutorialControl::waitForButton(const QString& buttonName) +{ + if (m_TargetControl != nullptr) { + QAbstractButton* button = m_TargetControl->findChild<QAbstractButton*>(buttonName); + if (button == nullptr) { + log::error("no button \"{}\" in control \"{}\"", buttonName, m_Name); + return false; + } + if (button->isEnabled()) { + m_Connections.append( + connect(button, SIGNAL(pressed()), this, SLOT(nextTutorialStepProxy()))); + lockUI(false); + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool TutorialControl::waitForTabOpen(const QString& tabControlName, const QString& tab) +{ + if (m_TargetControl != nullptr) { + QTabWidget* tabWidget = m_TargetControl->findChild<QTabWidget*>(tabControlName); + if (tabWidget == nullptr) { + log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name); + return false; + } + if (tabWidget->findChild<QWidget*>(tab) == nullptr) { + log::error("no widget \"{}\" found in tab widget \"{}\"", tab, tabControlName); + return false; + } + int tabIndex = tabWidget->indexOf(tabWidget->findChild<QWidget*>(tab)); + if (tabIndex == -1) { + log::error("widget \"{}\" does not appear to be a tab in tab widget \"{}\"", tab, + tabControlName); + return false; + } + if (tabWidget->isEnabled()) { + if (tabWidget->currentIndex() != tabIndex) { + m_ExpectedTab = tabIndex; + m_Connections.append(connect(tabWidget, SIGNAL(currentChanged(int)), this, + SLOT(tabChangedProxy(int)))); + lockUI(false); + } else { + QObject* background = m_TutorialView->rootObject(); + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + } + return true; + } else { + return false; + } + } else { + return false; + } +} + +const QString TutorialControl::getTabName(const QString& tabControlName) +{ + if (m_TargetControl != nullptr) { + QTabWidget* tabWidget = m_TargetControl->findChild<QTabWidget*>(tabControlName); + if (tabWidget == nullptr) { + log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name); + return QString(); + } + if (tabWidget->currentIndex() == -1) { + return QString(); + } else { + return tabWidget->currentWidget()->objectName(); + } + } else { + return QString(); + } +} +} // namespace MOBase diff --git a/libs/uibase/src/tutorialmanager.cpp b/libs/uibase/src/tutorialmanager.cpp new file mode 100644 index 0000000..58191e7 --- /dev/null +++ b/libs/uibase/src/tutorialmanager.cpp @@ -0,0 +1,112 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <uibase/tutorialmanager.h> +#include <uibase/log.h> +#include <uibase/tutorialcontrol.h> +#include <uibase/utility.h> +#include <QApplication> +#include <QDir> +#include <QList> +#include <QString> +#include <QTimer> + +namespace MOBase +{ + +TutorialManager* TutorialManager::s_Instance = nullptr; + +TutorialManager::TutorialManager(const QString& tutorialPath, QObject* organizerCore) + : m_TutorialPath(tutorialPath), m_OrganizerCore(organizerCore) +{} + +void TutorialManager::init(const QString& tutorialPath, QObject* organizerCore) +{ + if (s_Instance != nullptr) { + delete s_Instance; + } + s_Instance = new TutorialManager(tutorialPath, organizerCore); +} + +TutorialManager& TutorialManager::instance() +{ + if (s_Instance == nullptr) { + throw Exception(tr("tutorial manager not set up yet")); + } + return *s_Instance; +} + +void TutorialManager::activateTutorial(const QString& windowName, + const QString& tutorialName) +{ + std::map<QString, TutorialControl*>::iterator iter = m_Controls.find(windowName); + if (iter != m_Controls.end()) { + // control already visible, start tutorial right away + iter->second->startTutorial(m_TutorialPath + tutorialName); + } else { + m_PendingTutorials[windowName] = tutorialName; + } +} + +void TutorialManager::finishWindowTutorial(const QString& windowName) +{ + emit windowTutorialFinished(windowName); + // QSettings &settings = Settings::instance().directInterface(); + // settings.setValue(QString("CompletedWindowTutorials/") + windowName, true); +} + +bool TutorialManager::hasTutorial(const QString& tutorialName) +{ + return QFile::exists(m_TutorialPath + tutorialName); +} + +QWidget* TutorialManager::findControl(const QString& controlName) +{ + QWidget* mainWindow = qApp->activeWindow(); + if (mainWindow != nullptr) { + return mainWindow->findChild<QWidget*>(controlName); + } else { + return nullptr; + } +} + +void TutorialManager::registerControl(const QString& windowName, + TutorialControl* control) +{ + m_Controls[windowName] = control; + std::map<QString, QString>::iterator iter = m_PendingTutorials.find(windowName); + if (iter != m_PendingTutorials.end()) { + // there is a pending tutorial for this window, display it + QTimer::singleShot(0, [control, tutorial = m_TutorialPath + iter->second] { + control->startTutorial(tutorial); + }); + m_PendingTutorials.erase(iter); + } +} + +void TutorialManager::unregisterControl(const QString& windowName) +{ + std::map<QString, TutorialControl*>::iterator iter = m_Controls.find(windowName); + if (iter != m_Controls.end()) { + m_Controls.erase(iter); + } else { + log::warn("failed to remove tutorial control {}", windowName); + } +} +} // namespace MOBase diff --git a/libs/uibase/src/uibase_en.ts b/libs/uibase/src/uibase_en.ts new file mode 100644 index 0000000..defc545 --- /dev/null +++ b/libs/uibase/src/uibase_en.ts @@ -0,0 +1,407 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>FilterWidget</name> + <message> + <location filename="filterwidget.cpp" line="610"/> + <source>Filter options</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="617"/> + <source>Use regular expressions</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="618"/> + <source>Use regular expressions in filters</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="627"/> + <source>Case sensitive</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="629"/> + <source>Make regular expressions case sensitive (/i)</source> + <extracomment>leave "(/i)" verbatim</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="639"/> + <source>Extended</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="641"/> + <source>Ignores unescaped whitespace in regular expressions (/x)</source> + <extracomment>leave "(/x)" verbatim</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="651"/> + <source>Keep selection in view</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="filterwidget.cpp" line="652"/> + <source>Scroll to keep the current selection in view after filtering</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>FindDialog</name> + <message> + <location filename="finddialog.ui" line="14"/> + <source>Find</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="finddialog.ui" line="24"/> + <source>Find what:</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="finddialog.ui" line="31"/> + <location filename="finddialog.ui" line="34"/> + <source>Search term</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="finddialog.ui" line="47"/> + <location filename="finddialog.ui" line="50"/> + <source>Find next occurence from current file position.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="finddialog.ui" line="53"/> + <source>&Find Next</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="finddialog.ui" line="60"/> + <location filename="finddialog.ui" line="63"/> + <location filename="finddialog.ui" line="66"/> + <source>Close</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>QObject</name> + <message> + <location filename="filterwidget.cpp" line="128"/> + <source>Filter</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="pluginrequirements.cpp" line="31"/> + <source>One of the following plugins must be enabled: %1.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="pluginrequirements.cpp" line="34"/> + <source>This plugin can only be enabled if the '%1' plugin is installed and enabled.</source> + <translation type="unfinished"></translation> + </message> + <message numerus="yes"> + <location filename="pluginrequirements.cpp" line="63"/> + <source>This plugin can only be enabled for the following game(s): %1.</source> + <translation type="unfinished"> + <numerusform></numerusform> + <numerusform></numerusform> + </translation> + </message> + <message> + <location filename="registry.cpp" line="43"/> + <location filename="registry.cpp" line="44"/> + <location filename="textviewer.cpp" line="173"/> + <location filename="textviewer.cpp" line="174"/> + <source>INI file is read-only</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="registry.cpp" line="45"/> + <location filename="textviewer.cpp" line="175"/> + <source>Mod Organizer is attempting to write to "%1" which is currently set to read-only.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="registry.cpp" line="49"/> + <location filename="textviewer.cpp" line="179"/> + <source>Clear the read-only flag</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="registry.cpp" line="50"/> + <location filename="textviewer.cpp" line="180"/> + <source>Allow the write once</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="registry.cpp" line="51"/> + <location filename="textviewer.cpp" line="181"/> + <source>The file will be set to read-only again.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="registry.cpp" line="53"/> + <location filename="textviewer.cpp" line="183"/> + <source>Skip this file</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="report.cpp" line="65"/> + <location filename="report.cpp" line="71"/> + <source>Error</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="report.cpp" line="338"/> + <source>You can reset these choices by clicking "Reset Dialog Choices" in the General tab of the Settings</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="report.cpp" line="347"/> + <source>Always ask</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="report.cpp" line="348"/> + <location filename="report.cpp" line="356"/> + <source>Remember my choice</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="report.cpp" line="350"/> + <source>Remember my choice for %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="safewritefile.cpp" line="40"/> + <source>Failed to save '%1', could not create a temporary file: %2 (error %3)</source> + <oldsource>Failed to save '{}', could not create a temporary file: {} (error {})</oldsource> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="62"/> + <source>removal of "%1" failed: %2</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="71"/> + <source>removal of "%1" failed</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="75"/> + <source>"%1" doesn't exist (remove)</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="350"/> + <source>Error %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="534"/> + <location filename="utility.cpp" line="557"/> + <source>You have an invalid custom browser command in the settings.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="673"/> + <location filename="utility.cpp" line="702"/> + <source>failed to create directory "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="682"/> + <location filename="utility.cpp" line="709"/> + <source>failed to copy "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1134"/> + <source>%1 B</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1134"/> + <source>%1 KB</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1135"/> + <source>%1 MB</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1135"/> + <source>%1 GB</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1136"/> + <source>%1 TB</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1141"/> + <source>%1 B/s</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1141"/> + <source>%1 KB/s</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1142"/> + <source>%1 MB/s</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1142"/> + <source>%1 GB/s</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1143"/> + <source>%1 TB/s</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>QuestionBoxMemory</name> + <message> + <location filename="questionboxmemory.ui" line="106"/> + <source>Remember selection</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="questionboxmemory.ui" line="113"/> + <source>Remember selection only for %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TaskDialog</name> + <message> + <location filename="taskdialog.ui" line="20"/> + <source>Dialog</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="taskdialog.ui" line="56"/> + <source>icon</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="taskdialog.ui" line="102"/> + <source>dummy main text</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="taskdialog.ui" line="146"/> + <source>dummy content text</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="taskdialog.ui" line="206"/> + <source>dummy button</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="taskdialog.ui" line="234"/> + <source>dummy checkbox</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="taskdialog.ui" line="320"/> + <source>Details</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TextViewer</name> + <message> + <location filename="textviewer.ui" line="14"/> + <source>Log Viewer</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="textviewer.ui" line="22"/> + <source>Placeholder</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="textviewer.ui" line="29"/> + <source>Show Whitespace</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="textviewer.cpp" line="60"/> + <source>Save changes?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="textviewer.cpp" line="61"/> + <source>Do you want to save changes to %1?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="textviewer.cpp" line="192"/> + <source>failed to write to %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="textviewer.cpp" line="229"/> + <source>file not found: %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="textviewer.cpp" line="270"/> + <source>Save</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TutorialControl</name> + <message> + <location filename="tutorialcontrol.cpp" line="135"/> + <source>Tutorial failed to start, please check "mo_interface.log" for details.</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>TutorialManager</name> + <message> + <location filename="tutorialmanager.cpp" line="50"/> + <source>tutorial manager not set up yet</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>uibase</name> + <message> + <location filename="utility.cpp" line="1205"/> + <source>h</source> + <extracomment>Time remaining hours</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1208"/> + <source>m</source> + <extracomment>Time remaining minutes</extracomment> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="utility.cpp" line="1211"/> + <source>s</source> + <extracomment>Time remaining seconds</extracomment> + <translation type="unfinished"></translation> + </message> +</context> +</TS> diff --git a/libs/uibase/src/utility.cpp b/libs/uibase/src/utility.cpp new file mode 100644 index 0000000..0ad64ad --- /dev/null +++ b/libs/uibase/src/utility.cpp @@ -0,0 +1,1260 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include <uibase/utility.h> +#include <uibase/log.h> +#include <uibase/report.h> +#include <QApplication> +#include <QBuffer> +#include <QCollator> +#include <QDesktopServices> +#include <QDir> +#include <QDirIterator> +#include <QFileInfo> +#include <QImage> +#include <QImageReader> +#include <QLayout> +#include <QProcess> +#include <QRegularExpression> +#include <QScreen> +#include <QStandardPaths> +#include <QStringEncoder> +#include <QUrl> +#include <QUuid> +#include <QtDebug> +#include <cerrno> +#include <cstring> +#include <format> +#include <iostream> +#include <memory> +#include <sstream> + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> +#define FO_RECYCLE 0x1003 +#endif + +namespace MOBase +{ + +bool removeDir(const QString& dirName) +{ + QDir dir(dirName); + + if (dir.exists()) { + Q_FOREACH (QFileInfo info, + dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, + QDir::DirsFirst)) { + if (info.isDir()) { + if (!removeDir(info.absoluteFilePath())) { + return false; + } + } else { + // On Linux, just make sure file is writable before removing + QFile file(info.absoluteFilePath()); + file.setPermissions(file.permissions() | QFile::WriteUser); + if (!file.remove()) { + reportError(QObject::tr("removal of \"%1\" failed: %2") + .arg(info.absoluteFilePath()) + .arg(file.errorString())); + return false; + } + } + } + + if (!dir.rmdir(dirName)) { + reportError(QObject::tr("removal of \"%1\" failed").arg(dir.absolutePath())); + return false; + } + } else { + reportError(QObject::tr("\"%1\" doesn't exist (remove)").arg(dirName)); + return false; + } + + return true; +} + +bool copyDir(const QString& sourceName, const QString& destinationName, bool merge) +{ + QDir sourceDir(sourceName); + if (!sourceDir.exists()) { + return false; + } + QDir destDir(destinationName); + if (!destDir.exists()) { + destDir.mkdir(destinationName); + } else if (!merge) { + return false; + } + + QStringList files = sourceDir.entryList(QDir::Files); + foreach (QString fileName, files) { + QString srcName = sourceName + "/" + fileName; + QString destName = destinationName + "/" + fileName; + QFile::copy(srcName, destName); + } + + files.clear(); + // we leave out symlinks because that could cause an endless recursion + QStringList subDirs = + sourceDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + foreach (QString subDir, subDirs) { + QString srcName = sourceName + "/" + subDir; + QString destName = destinationName + "/" + subDir; + copyDir(srcName, destName, merge); + } + return true; +} + +// Linux shell operations use QFile/QDir instead of SHFileOperation + +static bool shellOpCopy(const QStringList& sourceNames, + const QStringList& destinationNames) +{ + // Multiple sources → single destination: treat destination as a directory + if (destinationNames.count() == 1 && sourceNames.count() > 1) { + QDir destDir(destinationNames[0]); + if (!destDir.exists()) { + destDir.mkpath("."); + } + for (const auto& src : sourceNames) { + QFileInfo srcInfo(src); + QString dest = destinationNames[0] + "/" + srcInfo.fileName(); + if (!QFile::copy(src, dest)) { + return false; + } + } + return true; + } + + // 1:1 or N:N — direct file-to-file copy + if (destinationNames.count() != sourceNames.count()) { + return false; + } + + for (int i = 0; i < sourceNames.count(); ++i) { + QFileInfo destInfo(destinationNames[i]); + if (!destInfo.dir().exists()) { + destInfo.dir().mkpath("."); + } + if (!QFile::copy(sourceNames[i], destinationNames[i])) { + return false; + } + } + return true; +} + +static bool shellOpMove(const QStringList& sourceNames, + const QStringList& destinationNames) +{ + // Multiple sources → single destination: treat destination as a directory + if (destinationNames.count() == 1 && sourceNames.count() > 1) { + QDir destDir(destinationNames[0]); + if (!destDir.exists()) { + destDir.mkpath("."); + } + for (const auto& src : sourceNames) { + QFileInfo srcInfo(src); + QString dest = destinationNames[0] + "/" + srcInfo.fileName(); + if (!QFile::rename(src, dest)) { + if (!QFile::copy(src, dest) || !QFile::remove(src)) { + return false; + } + } + } + return true; + } + + // 1:1 or N:N — direct file-to-file move + if (destinationNames.count() != sourceNames.count()) { + return false; + } + + for (int i = 0; i < sourceNames.count(); ++i) { + QFileInfo destInfo(destinationNames[i]); + if (!destInfo.dir().exists()) { + destInfo.dir().mkpath("."); + } + if (!QFile::rename(sourceNames[i], destinationNames[i])) { + if (!QFile::copy(sourceNames[i], destinationNames[i]) || + !QFile::remove(sourceNames[i])) { + return false; + } + } + } + return true; +} + +static bool shellOpDelete(const QStringList& fileNames, bool recycle) +{ + // On Linux, "recycle" moves to trash using Qt; otherwise just delete + for (const auto& fileName : fileNames) { + QFileInfo fi(fileName); + if (fi.isDir()) { + if (!removeDir(fileName)) { + return false; + } + } else { + if (recycle) { + if (!QFile::moveToTrash(fileName)) { + return false; + } + } else { + if (!QFile::remove(fileName)) { + return false; + } + } + } + } + return true; +} + +bool shellCopy(const QStringList& sourceNames, const QStringList& destinationNames, + QWidget* dialog) +{ + (void)dialog; + return shellOpCopy(sourceNames, destinationNames); +} + +bool shellCopy(const QString& sourceNames, const QString& destinationNames, + bool yesToAll, QWidget* dialog) +{ + (void)yesToAll; + (void)dialog; + return shellOpCopy(QStringList() << sourceNames, QStringList() << destinationNames); +} + +bool shellMove(const QStringList& sourceNames, const QStringList& destinationNames, + QWidget* dialog) +{ + (void)dialog; + return shellOpMove(sourceNames, destinationNames); +} + +bool shellMove(const QString& sourceNames, const QString& destinationNames, + bool yesToAll, QWidget* dialog) +{ + (void)yesToAll; + (void)dialog; + return shellOpMove(QStringList() << sourceNames, QStringList() << destinationNames); +} + +bool shellRename(const QString& oldName, const QString& newName, bool yesToAll, + QWidget* dialog) +{ + (void)yesToAll; + (void)dialog; + return QFile::rename(oldName, newName); +} + +bool shellDelete(const QStringList& fileNames, bool recycle, QWidget* dialog) +{ + (void)dialog; + return shellOpDelete(fileNames, recycle); +} + +namespace shell +{ + + static QString g_urlHandler; + + Result::Result(bool success, DWORD error, QString message, HANDLE process) + : m_success(success), m_error(error), m_message(std::move(message)), + m_process(process) + { + if (m_message.isEmpty()) { + m_message = QString::fromStdWString(formatSystemMessage(m_error)); + } + } + + Result Result::makeFailure(DWORD error, QString message) + { + return Result(false, error, std::move(message), INVALID_HANDLE_VALUE); + } + + Result Result::makeSuccess(HANDLE process) + { + return Result(true, ERROR_SUCCESS, {}, process); + } + + bool Result::success() const + { + return m_success; + } + + Result::operator bool() const + { + return m_success; + } + + DWORD Result::error() + { + return m_error; + } + + const QString& Result::message() const + { + return m_message; + } + + HANDLE Result::processHandle() const + { + return m_process.get(); + } + + HANDLE Result::stealProcessHandle() + { + const auto h = m_process.release(); + m_process.reset(INVALID_HANDLE_VALUE); + return h; + } + + QString Result::toString() const + { + if (m_message.isEmpty()) { + return QObject::tr("Error %1").arg(m_error); + } else { + return m_message; + } + } + + QString formatError(int i) + { + switch (i) { + case 0: + return "The operating system is out of memory or resources"; + case (int)ERROR_FILE_NOT_FOUND: + return "The specified file was not found"; + case (int)ERROR_PATH_NOT_FOUND: + return "The specified path was not found"; + case (int)ERROR_BAD_FORMAT: + return "The .exe file is invalid (non-Win32 .exe or error in .exe image)"; + case SE_ERR_ACCESSDENIED: + return "The operating system denied access to the specified file"; + case SE_ERR_ASSOCINCOMPLETE: + return "The file name association is incomplete or invalid"; + case SE_ERR_DDEBUSY: + return "The DDE transaction could not be completed because other DDE " + "transactions were being processed"; + case SE_ERR_DDEFAIL: + return "The DDE transaction failed"; + case SE_ERR_DDETIMEOUT: + return "The DDE transaction could not be completed because the request " + "timed out"; + case SE_ERR_DLLNOTFOUND: + return "The specified DLL was not found"; + case SE_ERR_NOASSOC: + return "There is no application associated with the given file name " + "extension"; + case SE_ERR_OOM: + return "There was not enough memory to complete the operation"; + case SE_ERR_SHARE: + return "A sharing violation occurred"; + default: + return QString("Unknown error %1").arg(i); + } + } + + Result ExploreDirectory(const QFileInfo& info) + { + const auto path = info.absoluteFilePath(); + // Use xdg-open on Linux + if (QProcess::startDetached("xdg-open", {path})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open directory")); + } + + Result ExploreFileInDirectory(const QFileInfo& info) + { + // Try to use the system file manager to highlight the file + // dbus method for Nautilus/Files, fallback to xdg-open on parent dir + const auto dir = info.absolutePath(); + if (QProcess::startDetached("xdg-open", {dir})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file manager")); + } + + Result Explore(const QFileInfo& info) + { + if (info.isFile()) { + return ExploreFileInDirectory(info); + } else if (info.isDir()) { + return ExploreDirectory(info); + } else { + const auto parent = info.dir(); + if (parent.exists()) { + return ExploreDirectory(QFileInfo(parent.absolutePath())); + } else { + return Result::makeFailure(ERROR_FILE_NOT_FOUND); + } + } + } + + Result Explore(const QString& path) + { + return Explore(QFileInfo(path)); + } + + Result Explore(const QDir& dir) + { + return Explore(QFileInfo(dir.absolutePath())); + } + + Result Open(const QString& path) + { + if (QProcess::startDetached("xdg-open", {path})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open file")); + } + + Result Open(const QUrl& url) + { + log::debug("opening url '{}'", url.toString()); + + if (g_urlHandler.isEmpty()) { + if (QProcess::startDetached("xdg-open", {url.toString()})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to open URL")); + } else { + // Custom URL handler + QString cmd = g_urlHandler; + cmd.replace("%1", url.toString()); + if (QProcess::startDetached("/bin/sh", {"-c", cmd})) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, + QObject::tr("You have an invalid custom browser command in the settings.")); + } + } + + Result Execute(const QString& program, const QString& params) + { + QStringList args; + if (!params.isEmpty()) { + args = QProcess::splitCommand(params); + } + if (QProcess::startDetached(program, args)) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_FILE_NOT_FOUND, QObject::tr("Failed to execute program")); + } + + void SetUrlHandler(const QString& cmd) + { + g_urlHandler = cmd; + } + + Result Delete(const QFileInfo& path) + { + if (QFile::remove(path.absoluteFilePath())) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_ACCESS_DENIED); + } + + Result Rename(const QFileInfo& src, const QFileInfo& dest) + { + return Rename(src, dest, true); + } + + Result Rename(const QFileInfo& src, const QFileInfo& dest, bool copyAllowed) + { + if (QFile::rename(src.absoluteFilePath(), dest.absoluteFilePath())) { + return Result::makeSuccess(); + } + + if (copyAllowed) { + if (QFile::copy(src.absoluteFilePath(), dest.absoluteFilePath())) { + QFile::remove(src.absoluteFilePath()); + return Result::makeSuccess(); + } + } + + return Result::makeFailure(ERROR_ACCESS_DENIED); + } + + Result CreateDirectories(const QDir& dir) + { + if (dir.mkpath(".")) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_ACCESS_DENIED, + QObject::tr("Failed to create directory: %1").arg(dir.path())); + } + + Result DeleteDirectoryRecursive(const QDir& dir) + { + if (shellOpDelete({dir.path()}, false)) { + return Result::makeSuccess(); + } + return Result::makeFailure(ERROR_ACCESS_DENIED); + } + +} // namespace shell + +bool moveFileRecursive(const QString& source, const QString& baseDir, + const QString& destination) +{ + QStringList pathComponents = destination.split("/"); + QString path = baseDir; + for (QStringList::Iterator iter = pathComponents.begin(); + iter != pathComponents.end() - 1; ++iter) { + path.append("/").append(*iter); + if (!QDir(path).exists() && !QDir().mkdir(path)) { + reportError(QObject::tr("failed to create directory \"%1\"").arg(path)); + return false; + } + } + + QString destinationAbsolute = baseDir.mid(0).append("/").append(destination); + if (!QFile::rename(source, destinationAbsolute)) { + // move failed, try copy & delete + if (!QFile::copy(source, destinationAbsolute)) { + reportError(QObject::tr("failed to copy \"%1\" to \"%2\"") + .arg(source) + .arg(destinationAbsolute)); + return false; + } else { + QFile::remove(source); + } + } + return true; +} + +bool copyFileRecursive(const QString& source, const QString& baseDir, + const QString& destination) +{ + QStringList pathComponents = destination.split("/"); + QString path = baseDir; + for (QStringList::Iterator iter = pathComponents.begin(); + iter != pathComponents.end() - 1; ++iter) { + path.append("/").append(*iter); + if (!QDir(path).exists() && !QDir().mkdir(path)) { + reportError(QObject::tr("failed to create directory \"%1\"").arg(path)); + return false; + } + } + + QString destinationAbsolute = baseDir.mid(0).append("/").append(destination); + if (!QFile::copy(source, destinationAbsolute)) { + reportError(QObject::tr("failed to copy \"%1\" to \"%2\"") + .arg(source) + .arg(destinationAbsolute)); + return false; + } + return true; +} + +std::wstring ToWString(const QString& source) +{ + return source.toStdWString(); +} + +std::string ToString(const QString& source, bool utf8) +{ + QByteArray array8bit; + if (utf8) { + array8bit = source.toUtf8(); + } else { + array8bit = source.toLocal8Bit(); + } + return std::string(array8bit.constData()); +} + +QString ToQString(const std::string& source) +{ + return QString::fromStdString(source); +} + +QString ToQString(const std::wstring& source) +{ + return QString::fromStdWString(source); +} + +bool isWindowsDrivePath(const QString& path) +{ + static const QRegularExpression re(R"(^\s*[A-Za-z]:[\\/].*)"); + return re.match(path).hasMatch(); +} + +bool isWineZDrivePath(const QString& path) +{ + static const QRegularExpression re(R"(^\s*[Zz]:[\\/].*)"); + return re.match(path).hasMatch(); +} + +QString toWinePath(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + QString p = path.trimmed(); + if (p.isEmpty()) { + return p; + } + + if (isWindowsDrivePath(p)) { + p.replace('/', '\\'); + return p; + } + + // Expand "~" on Linux before mapping to Z:. + if (p.startsWith("~")) { + p.replace(0, 1, QDir::homePath()); + } + + if (!QDir::isAbsolutePath(p)) { + p = QDir(p).absolutePath(); + } + + p = QDir::cleanPath(p); + p.replace('/', '\\'); + return "Z:" + p; +#endif +} + +QString fromWinePath(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + QString p = path.trimmed(); + if (p.isEmpty()) { + return p; + } + + if (!isWindowsDrivePath(p)) { + return QDir::cleanPath(p); + } + + // We only map Wine's Z: drive to host Linux absolute paths. + if (!isWineZDrivePath(p)) { + return p; + } + + p = p.mid(2); // strip "Z:" + p.replace('\\', '/'); + if (!p.startsWith('/')) { + p.prepend('/'); + } + + return QDir::cleanPath(p); +#endif +} + +QString normalizePathForHost(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + if (isWineZDrivePath(path)) { + return fromWinePath(path); + } + return QDir::cleanPath(path); +#endif +} + +QString normalizePathForWine(const QString& path) +{ +#ifdef _WIN32 + return QDir::toNativeSeparators(path); +#else + if (isWindowsDrivePath(path)) { + QString p = path; + p.replace('/', '\\'); + return p; + } + return toWinePath(path); +#endif +} + +#ifdef _WIN32 +QString ToString(const SYSTEMTIME& time) +{ + char dateBuffer[100]; + char timeBuffer[100]; + int size = 100; + GetDateFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, dateBuffer, + size); + GetTimeFormatA(LOCALE_USER_DEFAULT, LOCALE_USE_CP_ACP, &time, nullptr, timeBuffer, + size); + return QString::fromLocal8Bit(dateBuffer) + " " + QString::fromLocal8Bit(timeBuffer); +} +#endif + +static int naturalCompareI(const QString& a, const QString& b) +{ + static QCollator c = [] { + QCollator temp; + temp.setNumericMode(true); + temp.setCaseSensitivity(Qt::CaseInsensitive); + return temp; + }(); + + return c.compare(a, b); +} + +int naturalCompare(const QString& a, const QString& b, Qt::CaseSensitivity cs) +{ + if (cs == Qt::CaseInsensitive) { + return naturalCompareI(a, b); + } + + static QCollator c = [] { + QCollator temp; + temp.setNumericMode(true); + return temp; + }(); + + return c.compare(a, b); +} + +#ifdef _WIN32 +struct CoTaskMemFreer +{ + void operator()(void* p) { ::CoTaskMemFree(p); } +}; + +template <class T> +using COMMemPtr = std::unique_ptr<T, CoTaskMemFreer>; + +QString getOptionalKnownFolder(KNOWNFOLDERID id) +{ + COMMemPtr<wchar_t> path; + { + wchar_t* rawPath = nullptr; + HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath); + if (FAILED(res)) { + return {}; + } + path.reset(rawPath); + } + return QString::fromWCharArray(path.get()); +} + +QDir getKnownFolder(KNOWNFOLDERID id, const QString& what) +{ + COMMemPtr<wchar_t> path; + { + wchar_t* rawPath = nullptr; + HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &rawPath); + if (FAILED(res)) { + log::error("failed to get known folder '{}', {}", + what.isEmpty() ? QUuid(id).toString() : what, + formatSystemMessage(res)); + throw std::runtime_error("couldn't get known folder path"); + } + path.reset(rawPath); + } + return QString::fromWCharArray(path.get()); +} +#endif + +QString getDesktopDirectory() +{ +#ifdef _WIN32 + return getKnownFolder(FOLDERID_Desktop, "desktop").absolutePath(); +#else + return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +#endif +} + +QString getStartMenuDirectory() +{ +#ifdef _WIN32 + return getKnownFolder(FOLDERID_StartMenu, "start menu").absolutePath(); +#else + return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); +#endif +} + +bool shellDeleteQuiet(const QString& fileName, QWidget* dialog) +{ + if (!QFile::remove(fileName)) { + return shellDelete(QStringList(fileName), false, dialog); + } + return true; +} + +QString readFileText(const QString& fileName, QString* encoding, bool* hadBOM) +{ + + QFile textFile(fileName); + if (!textFile.open(QIODevice::ReadOnly)) { + return QString(); + } + + QByteArray buffer = textFile.readAll(); + return decodeTextData(buffer, encoding, hadBOM); +} + +QString decodeTextData(const QByteArray& fileData, QString* encoding, bool* hadBOM) +{ + QStringConverter::Encoding codec = QStringConverter::Encoding::Utf8; + QStringEncoder encoder(codec); + QStringDecoder decoder(codec, QStringConverter::Flag::ConvertInitialBom); + QString text = decoder.decode(fileData); + + // embedded nulls probably mean it was UTF-16 - they're rare/illegal in text files + bool hasEmbeddedNulls = false; + for (const auto& character : text) { + if (character.isNull()) { + hasEmbeddedNulls = true; + break; + } + } + + // check reverse conversion + if (hasEmbeddedNulls || encoder.encode(text) != fileData) { + log::debug("conversion failed assuming local encoding"); + auto codecSearch = QStringConverter::encodingForData(fileData); + if (codecSearch.has_value()) { + codec = codecSearch.value(); + decoder = QStringDecoder(codec, QStringConverter::Flag::ConvertInitialBom); + } else { + decoder = QStringDecoder(hasEmbeddedNulls ? QStringConverter::Encoding::Utf16 + : QStringConverter::Encoding::System); + } + text = decoder.decode(fileData); + } + + if (encoding != nullptr) { + *encoding = QStringConverter::nameForEncoding(codec); + } + + if (!text.isEmpty() && text.startsWith(QChar::ByteOrderMark)) { + text.remove(0, 1); + + if (hadBOM != nullptr) { + *hadBOM = true; + } + } else if (hadBOM != nullptr) { + *hadBOM = false; + } + + return text; +} + +void removeOldFiles(const QString& path, const QString& pattern, int numToKeep, + QDir::SortFlags sorting) +{ + QFileInfoList files = + QDir(path).entryInfoList(QStringList(pattern), QDir::Files, sorting); + + if (files.count() > numToKeep) { + QStringList deleteFiles; + for (int i = 0; i < files.count() - numToKeep; ++i) { + deleteFiles.append(files.at(i).absoluteFilePath()); + } + + if (!shellDelete(deleteFiles)) { + log::warn("failed to remove log files"); + } + } +} + +QIcon iconForExecutable(const QString& filePath) +{ + static QHash<QString, QIcon> cache; + + const QFileInfo fi(filePath); + if (!fi.exists()) { + return QIcon(":/MO/gui/executable"); + } + + const QString cacheKey = + fi.canonicalFilePath() + "|" + QString::number(fi.size()) + "|" + + fi.lastModified().toString(Qt::ISODateWithMs); + + if (cache.contains(cacheKey)) { + return cache.value(cacheKey); + } + +#ifdef _WIN32 + QIcon icon(":/MO/gui/executable"); + cache.insert(cacheKey, icon); + return icon; +#else + // Try to extract icon resources from PE executables via icoutils/wrestool. + // If this fails, return a generic executable icon. + QIcon icon; + + const QString wrestool = QStandardPaths::findExecutable("wrestool"); + if (!wrestool.isEmpty()) { + const QString cacheRoot = + QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + "/exe_icons"; + QDir().mkpath(cacheRoot); + + const QString outDirPath = cacheRoot + "/" + QString::number(qHash(cacheKey), 16); + QDir outDir(outDirPath); + if (!outDir.exists()) { + QDir().mkpath(outDirPath); + } + + auto findBestIcon = [&](const QString& dirPath) -> QIcon { + QString bestFile; + int bestArea = -1; + + QDirIterator it(dirPath, + QStringList() << "*.png" + << "*.ico" + << "*.bmp", + QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + const QString path = it.next(); + QImageReader reader(path); + const QSize size = reader.size(); + const int area = size.width() * size.height(); + if (area > bestArea) { + bestArea = area; + bestFile = path; + } else if (bestArea < 0) { + // Unknown image size (e.g. multi-size ico); keep first viable file. + bestFile = path; + } + } + + if (!bestFile.isEmpty()) { + QIcon candidate(bestFile); + if (!candidate.pixmap(24, 24).isNull() || !candidate.pixmap(32, 32).isNull()) { + return candidate; + } + } + + return QIcon(); + }; + + // Reuse previous extraction if present. + icon = findBestIcon(outDirPath); + + if (icon.isNull()) { + const QList<QStringList> tries = { + {"-x", "--type=group_icon", "-o", outDirPath, fi.absoluteFilePath()}, + {"-x", "--type=14", "-o", outDirPath, fi.absoluteFilePath()}, + {"-x", "-t14", "-o", outDirPath, fi.absoluteFilePath()}, + }; + + for (const auto& args : tries) { + QProcess p; + p.start(wrestool, args); + if (!p.waitForFinished(4000)) { + p.kill(); + continue; + } + if (p.exitStatus() == QProcess::NormalExit && p.exitCode() == 0) { + icon = findBestIcon(outDirPath); + if (!icon.isNull()) { + break; + } + } + } + } + } + + if (icon.isNull()) { + icon = QIcon(":/MO/gui/executable"); + } + + cache.insert(cacheKey, icon); + return icon; +#endif +} + +QString getFileVersion(QString const& filepath) +{ + // On Linux, there is no Windows PE file version. Return empty. + (void)filepath; + return ""; +} + +QString getProductVersion(QString const& filepath) +{ + // On Linux, there is no Windows PE product version. Return empty. + (void)filepath; + return ""; +} + +void deleteChildWidgets(QWidget* w) +{ + auto* ly = w->layout(); + if (!ly) { + return; + } + + while (auto* item = ly->takeAt(0)) { + delete item->widget(); + delete item; + } +} + +void trimWString(std::wstring& s) +{ + s.erase(std::remove_if(s.begin(), s.end(), + [](wint_t ch) { + return std::iswspace(ch); + }), + s.end()); +} + +std::wstring formatMessage(DWORD id, const std::wstring& message) +{ + std::wstring s; + + std::wostringstream oss; + oss << L"0x" << std::hex << id; + + if (message.empty()) { + s = oss.str(); + } else { + s += message + L" (" + oss.str() + L")"; + } + + return s; +} + +std::wstring formatSystemMessage(DWORD id) +{ +#ifdef _WIN32 + std::wstring getMessage(DWORD id, HMODULE mod); + return formatMessage(id, getMessage(id, 0)); +#else + // On Linux, map common error codes to strings or use strerror for errno values + if (id == 0) { + return L"Success"; + } + // If it looks like an errno value (small numbers), use strerror + if (id < 200) { + const char* msg = strerror(static_cast<int>(id)); + if (msg) { + std::string s(msg); + return std::wstring(s.begin(), s.end()); + } + } + return formatMessage(id, L""); +#endif +} + +std::wstring formatNtMessage(NTSTATUS s) +{ +#ifdef _WIN32 + const DWORD id = static_cast<DWORD>(s); + std::wstring getMessage(DWORD id, HMODULE mod); + return formatMessage(id, getMessage(id, ::GetModuleHandleW(L"ntdll.dll"))); +#else + return formatMessage(static_cast<DWORD>(s), L"NT status"); +#endif +} + +QString windowsErrorString(DWORD errorCode) +{ + return QString::fromStdWString(formatSystemMessage(errorCode)); +} + +QString localizedSize(unsigned long long bytes, const QString& B, const QString& KB, + const QString& MB, const QString& GB, const QString& TB) +{ + constexpr unsigned long long OneKB = 1024ull; + constexpr unsigned long long OneMB = 1024ull * 1024; + constexpr unsigned long long OneGB = 1024ull * 1024 * 1024; + constexpr unsigned long long OneTB = 1024ull * 1024 * 1024 * 1024; + + auto makeNum = [&](int factor) { + const double n = static_cast<double>(bytes) / std::pow(1024.0, factor); + const double truncated = + static_cast<double>(static_cast<unsigned long long>(n * 100)) / 100.0; + return QString().setNum(truncated, 'f', 2); + }; + + if (bytes < OneKB) { + return B.arg(bytes); + } else if (bytes < OneMB) { + return KB.arg(makeNum(1)); + } else if (bytes < OneGB) { + return MB.arg(makeNum(2)); + } else if (bytes < OneTB) { + return GB.arg(makeNum(3)); + } else { + return TB.arg(makeNum(4)); + } +} + +QDLLEXPORT QString localizedByteSize(unsigned long long bytes) +{ + return localizedSize(bytes, QObject::tr("%1 B"), QObject::tr("%1 KB"), + QObject::tr("%1 MB"), QObject::tr("%1 GB"), + QObject::tr("%1 TB")); +} + +QDLLEXPORT QString localizedByteSpeed(unsigned long long bps) +{ + return localizedSize(bps, QObject::tr("%1 B/s"), QObject::tr("%1 KB/s"), + QObject::tr("%1 MB/s"), QObject::tr("%1 GB/s"), + QObject::tr("%1 TB/s")); +} + +QDLLEXPORT QString localizedTimeRemaining(unsigned int remaining) +{ + QString Result; + double interval; + qint64 intval; + + // Hours + interval = 60.0 * 60.0 * 1000.0; + intval = (qint64)trunc((double)remaining / interval); + if (intval < 0) + intval = 0; + remaining -= static_cast<unsigned int>(trunc((double)intval * interval)); + qint64 hours = intval; + + // Minutes + interval = 60.0 * 1000.0; + intval = (qint64)trunc((double)remaining / interval); + if (intval < 0) + intval = 0; + remaining -= static_cast<unsigned int>(trunc((double)intval * interval)); + qint64 minutes = intval; + + // Seconds + interval = 1000.0; + intval = (qint64)trunc((double)remaining / interval); + if (intval < 0) + intval = 0; + remaining -= static_cast<unsigned int>(trunc((double)intval * interval)); + qint64 seconds = intval; + + char buffer[25]; + memset(buffer, 0, 25); + + if (hours > 0) { + if (hours < 10) + snprintf(buffer, sizeof(buffer), "0%lld", (long long)hours); + else + snprintf(buffer, sizeof(buffer), "%lld", (long long)hours); + Result.append(QString("%1:").arg(buffer)); + } + + if (minutes > 0 || hours > 0) { + if (minutes < 10 && hours > 0) + snprintf(buffer, sizeof(buffer), "0%lld", (long long)minutes); + else + snprintf(buffer, sizeof(buffer), "%lld", (long long)minutes); + Result.append(QString("%1:").arg(buffer)); + } + + if (seconds < 10 && (minutes > 0 || hours > 0)) + snprintf(buffer, sizeof(buffer), "0%lld", (long long)seconds); + else + snprintf(buffer, sizeof(buffer), "%lld", (long long)seconds); + Result.append(QString("%1").arg(buffer)); + + if (hours > 0) + //: Time remaining hours + Result.append(QApplication::translate("uibase", "h")); + else if (minutes > 0) + //: Time remaining minutes + Result.append(QApplication::translate("uibase", "m")); + else + //: Time remaining seconds + Result.append(QApplication::translate("uibase", "s")); + + return Result; +} + +QDLLEXPORT void localizedByteSizeTests() +{ + auto f = [](unsigned long long n) { + return localizedByteSize(n).toStdString(); + }; + +#define CHECK_EQ(a, b) \ + if ((a) != (b)) { \ + std::cerr << "failed: " << a << " == " << b << "\n"; \ + } + + CHECK_EQ(f(0), "0 B"); + CHECK_EQ(f(1), "1 B"); + CHECK_EQ(f(999), "999 B"); + CHECK_EQ(f(1000), "1000 B"); + CHECK_EQ(f(1023), "1023 B"); + + CHECK_EQ(f(1024), "1.00 KB"); + CHECK_EQ(f(2047), "1.99 KB"); + CHECK_EQ(f(2048), "2.00 KB"); + CHECK_EQ(f(1048575), "1023.99 KB"); + + CHECK_EQ(f(1048576), "1.00 MB"); + CHECK_EQ(f(1073741823), "1023.99 MB"); + + CHECK_EQ(f(1073741824), "1.00 GB"); + CHECK_EQ(f(1099511627775), "1023.99 GB"); + + CHECK_EQ(f(1099511627776), "1.00 TB"); + CHECK_EQ(f(2759774185818), "2.51 TB"); + +#undef CHECK_EQ +} + +TimeThis::TimeThis(const QString& what) : m_running(false) +{ + start(what); +} + +TimeThis::~TimeThis() +{ + stop(); +} + +void TimeThis::start(const QString& what) +{ + stop(); + + m_what = what; + m_start = Clock::now(); + m_running = true; +} + +void TimeThis::stop() +{ + using namespace std::chrono; + + if (!m_running) { + return; + } + + const auto end = Clock::now(); + const auto d = duration_cast<milliseconds>(end - m_start).count(); + + if (m_what.isEmpty()) { + log::debug("timing: {} ms", d); + } else { + log::debug("timing: {} {} ms", m_what, d); + } + + m_running = false; +} + +} // namespace MOBase diff --git a/libs/uibase/src/version.rc b/libs/uibase/src/version.rc new file mode 100644 index 0000000..fdea5f9 --- /dev/null +++ b/libs/uibase/src/version.rc @@ -0,0 +1,37 @@ +#include "Winver.h" + +// If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number. +// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser +// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha +#define VER_FILEVERSION 2.5.3 +#define VER_FILEVERSION_STR "2.5.3\0" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION VER_FILEVERSION +PRODUCTVERSION VER_FILEVERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +FILEFLAGS (0) +FILEOS VOS__WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE (0) +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "CompanyName", "Mod Organizer 2 Team\0" + VALUE "FileDescription", "MO2 Base UI plugin API library\0" + VALUE "OriginalFilename", "uibase.dll\0" + VALUE "InternalName", "uibase\0" + VALUE "LegalCopyright", "Copyright 2011-2016 Sebastian Herbord\r\nCopyright 2016-2026 Mod Organizer 2 contributors\0" + VALUE "ProductName", "Mod Organizer 2 UI Base\0" + VALUE "ProductVersion", VER_FILEVERSION_STR + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END +END diff --git a/libs/uibase/src/versioninfo.cpp b/libs/uibase/src/versioninfo.cpp new file mode 100644 index 0000000..6dec18e --- /dev/null +++ b/libs/uibase/src/versioninfo.cpp @@ -0,0 +1,432 @@ +/* +Mod Organizer shared UI functionality + +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include <uibase/versioninfo.h> +#include <QDate> +#include <QLocale> +#include <QRegularExpression> +#include <QVersionNumber> + +namespace +{ +const QRegularExpression VERSION_REGEX("^(\\d+)(\\.(\\d+))?(\\.(\\d+))?(\\.(\\d+))?"); +} + +namespace MOBase +{ + +VersionInfo::VersionInfo() + : m_Scheme(SCHEME_REGULAR), m_Valid(false), m_ReleaseType(RELEASE_FINAL), + m_Major(0), m_Minor(0), m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), + m_Rest() +{} + +VersionInfo::VersionInfo(int major, int minor, int subminor, int subsubminor, + ReleaseType releaseType) + : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType), + m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(subsubminor), + m_DecimalPositions(0), m_Rest() +{} + +VersionInfo::VersionInfo(int major, int minor, int subminor, ReleaseType releaseType) + : m_Scheme(SCHEME_REGULAR), m_Valid(true), m_ReleaseType(releaseType), + m_Major(major), m_Minor(minor), m_SubMinor(subminor), m_SubSubMinor(0), + m_DecimalPositions(0), m_Rest() +{} + +VersionInfo::VersionInfo(const QString& versionString, VersionScheme scheme) + : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0), + m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest() +{ + parse(versionString, scheme); +} + +VersionInfo::VersionInfo(const QString& versionString, + VersionInfo::VersionScheme scheme, bool manualInput) + : m_Valid(true), m_ReleaseType(RELEASE_FINAL), m_Major(0), m_Minor(0), + m_SubMinor(0), m_SubSubMinor(0), m_DecimalPositions(0), m_Rest() +{ + parse(versionString, scheme, manualInput); +} + +void VersionInfo::clear() +{ + m_Scheme = SCHEME_REGULAR; + m_Valid = false; + m_ReleaseType = RELEASE_FINAL; + m_Major = m_Minor = m_SubMinor = m_SubSubMinor = m_DecimalPositions = 0; + m_Rest.clear(); +} + +QString VersionInfo::canonicalString() const +{ + if (!isValid()) { + return QString(); + } + + QString result; + if (m_Scheme == SCHEME_REGULAR) { + result = QString("%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (m_Scheme == SCHEME_DECIMALMARK) { + result = QString("f%1.%2").arg(m_Major).arg( + QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0')); + } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) { + result = QString("n%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (m_Scheme == SCHEME_DATE) { + // year.month.day was stored in the version fields + result = QString("d%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } + switch (m_ReleaseType) { + case RELEASE_PREALPHA: { + result.append(" pre-alpha"); + } break; + case RELEASE_ALPHA: { + result.append("a"); + } break; + case RELEASE_BETA: { + result.append("b"); + } break; + case RELEASE_CANDIDATE: { + result.append("rc"); + } break; + case RELEASE_FINAL: // fall-through + default: { + // nop + } break; + } + + if (!m_Rest.isEmpty()) { + result.append(QString("%1").arg(m_Rest)); + } + + return result; +} + +QString VersionInfo::displayString(int forcedVersionSegments) const +{ + if (!isValid()) { + return QString(); + } + + QString result; + if (m_Scheme == SCHEME_REGULAR) { + if (forcedVersionSegments >= 4 || m_SubSubMinor != 0) { + result = QString("%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (forcedVersionSegments == 3 || m_SubMinor != 0) { + result = QString("%1.%2.%3").arg(m_Major).arg(m_Minor).arg(m_SubMinor); + } else { + result = QString("%1.%2").arg(m_Major).arg(m_Minor); + } + } else if (m_Scheme == SCHEME_DECIMALMARK) { + result = QString("%1.%2").arg(m_Major).arg( + QString("%1").arg(m_Minor).rightJustified(m_DecimalPositions, '0')); + } else if (m_Scheme == SCHEME_NUMBERSANDLETTERS) { + result = QString("%1.%2.%3.%4") + .arg(m_Major) + .arg(m_Minor) + .arg(m_SubMinor) + .arg(m_SubSubMinor); + } else if (m_Scheme == SCHEME_DATE) { + // year.month.day was stored in the version fields + const auto year = m_Major; + const auto month = m_Minor; + const auto day = m_SubMinor; + + return QLocale::system().toString(QDate(year, month, day), + QLocale::FormatType::ShortFormat); + } + switch (m_ReleaseType) { + case RELEASE_PREALPHA: { + result.append(" pre-alpha"); + } break; + case RELEASE_ALPHA: { + result.append("alpha"); + } break; + case RELEASE_BETA: { + result.append("beta"); + } break; + case RELEASE_CANDIDATE: { + result.append("rc"); + } break; + case RELEASE_FINAL: // fall-through + default: { + // nop + } break; + } + + if (!m_Rest.isEmpty()) { + result.append(QString("%1").arg(m_Rest)); + } + + return result; +} + +QVersionNumber VersionInfo::asQVersionNumber() const +{ + return QVersionNumber::fromString(displayString()).normalized(); +} + +QString VersionInfo::parseReleaseType(QString versionString) +{ + // release types are often followed by a number (i.e. "beta4"). This needs to be + // extracted now, otherwise the outer parser will think it's the subminor version and + // then 1.0.0rc1 would be interpreted as newer than 1.0.0 + + static std::map<QString, ReleaseType> typeStrings = {{"prealpha", RELEASE_PREALPHA}, + {"alpha", RELEASE_ALPHA}, + {"beta", RELEASE_BETA}, + {"rc", RELEASE_CANDIDATE}}; + + m_ReleaseType = RELEASE_FINAL; + + auto typeIter = typeStrings.begin(); + int offset = -1; + + for (; (typeIter != typeStrings.end()) && (offset == -1); ++typeIter) { + offset = (int)versionString.indexOf(typeIter->first, Qt::CaseInsensitive); + if (offset != -1) { + m_ReleaseType = typeIter->second; + break; + } + } + + int length = 0; + + if (typeIter != typeStrings.end()) { + length = (int)typeIter->first.length(); + } + + if (m_Scheme == SCHEME_REGULAR) { + // also interpret the a/b letters, but only if they follow immediately on the + // version number, otherwise the margin for error is too big + if ((offset == -1) && (versionString.length() > 0)) { + if (versionString.at(0) == 'a') { + m_ReleaseType = RELEASE_ALPHA; + offset = 0; + length = 1; + } else if (versionString.at(0) == 'b') { + m_ReleaseType = RELEASE_BETA; + offset = 0; + length = 1; + } + } + } + + if (offset != -1) { + versionString.remove(offset, length); + } + return versionString.trimmed(); +} + +void VersionInfo::parse(const QString& versionString, VersionScheme scheme, + bool manualInput) +{ + m_Valid = false; + m_Scheme = scheme == SCHEME_LITERAL ? SCHEME_REGULAR + : scheme != SCHEME_DISCOVER ? scheme + : SCHEME_REGULAR; + m_ReleaseType = RELEASE_FINAL; + m_Major = m_Minor = m_SubMinor = m_SubSubMinor = 0; + m_Rest.clear(); + if (versionString.length() == 0) { + return; + } + + if (QString::compare(versionString, "final", Qt::CaseInsensitive) == 0) { + m_Major = 1; + m_Valid = true; + return; + } + + QString temp = versionString; + // first, determine the versioning scheme if there is a hint + VersionScheme newScheme = m_Scheme; + if (!manualInput) { + if (temp.startsWith('f')) { + newScheme = SCHEME_DECIMALMARK; + temp.remove(0, 1); + } else if (temp.startsWith('n')) { + newScheme = SCHEME_NUMBERSANDLETTERS; + temp.remove(0, 1); + } else if (temp.startsWith('d')) { + newScheme = SCHEME_DATE; + temp.remove(0, 1); + } + } + + if (scheme == SCHEME_DISCOVER) { + m_Scheme = newScheme; + } + + if (temp.startsWith('v', Qt::CaseInsensitive)) { + // v is often prepended to versions + temp.remove(0, 1); + } + + auto match = VERSION_REGEX.match(temp); + if (match.hasMatch()) { + m_Major = match.captured(1).toInt(); + m_Minor = match.captured(3).toInt(); + QString subMinor = match.captured(5); + QString subSubMinor = match.captured(7); + if (!subMinor.isEmpty() && (m_Scheme == SCHEME_DECIMALMARK)) { + // nooooope, if there are two dots it can't be a decimal mark + m_Scheme = SCHEME_REGULAR; + } + if (m_Scheme != SCHEME_DECIMALMARK) { + m_SubMinor = subMinor.toInt(); + m_SubSubMinor = subSubMinor.toInt(); + } + if (subMinor.isEmpty() && (match.captured(3).size() > 1) && + match.captured(3).startsWith('0')) { + // this indicates a decimal scheme + m_Scheme = SCHEME_DECIMALMARK; + m_DecimalPositions = (int)match.captured(3).size(); + } + temp.remove(VERSION_REGEX); + } else { + m_Scheme = SCHEME_LITERAL; + } + + if (m_Scheme == SCHEME_REGULAR) { + temp = parseReleaseType(temp); + } + + if ((m_Scheme == SCHEME_DATE) && (m_Major < 1900)) { + m_Scheme = SCHEME_REGULAR; + } + m_Rest = temp.trimmed(); + m_Valid = true; +} + +QDLLEXPORT bool operator<(const VersionInfo& LHS, const VersionInfo& RHS) +{ + if (!LHS.isValid() && RHS.isValid()) + return true; + if (!RHS.isValid() && LHS.isValid()) + return false; + + // date-releases are lower than regular versions + if ((LHS.m_Scheme == VersionInfo::SCHEME_DATE) && + (RHS.m_Scheme != VersionInfo::SCHEME_DATE)) { + return true; + } else if ((LHS.m_Scheme != VersionInfo::SCHEME_DATE) && + (RHS.m_Scheme == VersionInfo::SCHEME_DATE)) { + return false; + } else if ((LHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK) || + (RHS.m_Scheme == VersionInfo::SCHEME_DECIMALMARK)) { + // use decimal versioning if either version is a decimal. The parser interprets + // versions as regular if in doubt so if the scheme is "decimal" it is definitively + // a decimal version number whereas SCHEME_REGULAR means "probably regular" + + float leftVal = QString("%1.%2") + .arg(LHS.m_Major) + .arg(QString("%1") + .arg(LHS.m_Minor) + .rightJustified(LHS.m_DecimalPositions, '0')) + .toFloat(); + float rightVal = QString("%1.%2") + .arg(RHS.m_Major) + .arg(QString("%1") + .arg(RHS.m_Minor) + .rightJustified(RHS.m_DecimalPositions, '0')) + .toFloat(); + if (fabs(leftVal - rightVal) > 0.001f) { + return leftVal < rightVal; + } + } else { + // if in doubt, use the sane choice. regular and numbers+letters can be treated the + // same way + if (LHS.m_Major != RHS.m_Major) + return LHS.m_Major < RHS.m_Major; + if (LHS.m_Minor != RHS.m_Minor) + return LHS.m_Minor < RHS.m_Minor; + if (LHS.m_SubMinor != RHS.m_SubMinor) + return LHS.m_SubMinor < RHS.m_SubMinor; + if (LHS.m_SubSubMinor != RHS.m_SubSubMinor) + return LHS.m_SubSubMinor < RHS.m_SubSubMinor; + } + + // subminor, release-type and rest are treated the same for all versioning schemes, + // but on parsing they may still differ, i.e. a b-suffix is only interpreted to mean + // "beta" in the regular scheme + if (LHS.m_ReleaseType != RHS.m_ReleaseType) + return LHS.m_ReleaseType < RHS.m_ReleaseType; + + // if the rest contains only integers, compare them numerically + bool LHS_ok, RHS_ok; + int LHS_int, RHS_int; + LHS_int = LHS.m_Rest.toInt(&LHS_ok); + RHS_int = RHS.m_Rest.toInt(&RHS_ok); + if (LHS_ok && RHS_ok) { + return LHS_int < RHS_int; + } + + // give up and compare lexically + return LHS.m_Rest < RHS.m_Rest; +} + +QDLLEXPORT bool operator>(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return !(LHS <= RHS); +} + +QDLLEXPORT bool operator<=(const VersionInfo& LHS, const VersionInfo& RHS) +{ + // TODO not exactly optimized... + if (LHS < RHS) { + return true; + } else { + return !(RHS < LHS); + } +} + +QDLLEXPORT bool operator>=(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return RHS <= LHS; +} + +QDLLEXPORT bool operator!=(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return (LHS < RHS) || (RHS < LHS); +} + +QDLLEXPORT bool operator==(const VersionInfo& LHS, const VersionInfo& RHS) +{ + return !(LHS < RHS) && !(RHS < LHS); +} + +} // namespace MOBase diff --git a/libs/uibase/src/versioning.cpp b/libs/uibase/src/versioning.cpp new file mode 100644 index 0000000..8178841 --- /dev/null +++ b/libs/uibase/src/versioning.cpp @@ -0,0 +1,278 @@ +#include <uibase/versioning.h> + +#include <QRegularExpression> +#include <format> +#include <unordered_map> + +#include <uibase/formatters.h> + +// official semver regex +static const QRegularExpression s_SemVerStrictRegEx{ + R"(^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX +static const QRegularExpression s_SemVerMO2RegEx{ + R"(^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:\.(?P<subpatch>0|[1-9]\d*))?(?:(?P<type>dev|a|alpha|b|beta|rc)(?P<prerelease>0|[1-9](?:[.0-9])*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"}; + +// match from value to release type +static const std::unordered_map<QString, MOBase::Version::ReleaseType> + s_StringToRelease{{"dev", MOBase::Version::Development}, + {"alpha", MOBase::Version::Alpha}, + {"a", MOBase::Version::Alpha}, + {"beta", MOBase::Version::Beta}, + {"b", MOBase::Version::Beta}, + {"rc", MOBase::Version::ReleaseCandidate}}; + +namespace MOBase +{ + +namespace +{ + + Version parseVersionSemVer(QString const& value) + { + const auto match = s_SemVerStrictRegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + std::vector<std::variant<int, Version::ReleaseType>> prereleases; + for (auto& part : match.captured("prerelease") + .split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) { + // try to extract an int + bool ok = true; + const auto intValue = part.toInt(&ok); + if (ok) { + prereleases.push_back(intValue); + continue; + } + + // check if we have a valid prerelease type + const auto it = s_StringToRelease.find(part.toLower()); + if (it == s_StringToRelease.end()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid prerelease type: '{}'", part))); + } + + prereleases.push_back(it->second); + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, 0, prereleases, buildMetadata); + } + + Version parseVersionMO2(QString const& value) + { + const auto match = s_SemVerMO2RegEx.match(value); + + if (!match.hasMatch()) { + throw InvalidVersionException( + QString::fromStdString(std::format("invalid version string: '{}'", value))); + } + + const auto major = match.captured("major").toInt(); + const auto minor = match.captured("minor").toInt(); + const auto patch = match.captured("patch").toInt(); + + const auto subpatch = match.captured("subpatch").toInt(); + + // unlike semver, the regex will only match valid values + std::vector<std::variant<int, Version::ReleaseType>> prereleases; + if (match.hasCaptured("type")) { + prereleases.push_back(s_StringToRelease.at(match.captured("type"))); + + // for version with decimal point, e.g., 2.4.1rc1.1, we split the components into + // pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc, + // 1, 1} + // + for (const auto& preVersion : + match.captured("prerelease").split(".", Qt::SkipEmptyParts)) { + prereleases.push_back(preVersion.toInt()); + } + } + + const auto buildMetadata = match.captured("buildmetadata").trimmed(); + + return Version(major, minor, patch, subpatch, prereleases, buildMetadata); + } + +} // namespace + +Version Version::parse(QString const& value, ParseMode mode) +{ + return mode == ParseMode::SemVer ? parseVersionSemVer(value) : parseVersionMO2(value); +} + +// constructors + +Version::Version(int major, int minor, int patch, QString metadata) + : Version(major, minor, patch, 0, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, QString metadata) + : Version(major, minor, patch, 0, type, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{type}, m_BuildMetadata{std::move(metadata)} +{} + +Version::Version(int major, int minor, int patch, ReleaseType type, int prerelease, + QString metadata) + : Version(major, minor, patch, 0, type, prerelease, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata) + : Version(major, minor, patch, subpatch, {type, prerelease}, std::move(metadata)) +{} + +Version::Version(int major, int minor, int patch, int subpatch, + std::vector<std::variant<int, ReleaseType>> prereleases, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{std::move(prereleases)}, m_BuildMetadata{std::move(metadata)} +{} + +// string + +QString Version::string(const FormatModes& modes) const +{ + const bool noSeparator = modes.testFlag(FormatMode::NoSeparator); + const bool shortAlphaBeta = modes.testFlag(FormatMode::ShortAlphaBeta); + auto value = std::format("{}.{}.{}", m_Major, m_Minor, m_Patch); + + if (m_SubPatch || modes.testFlag(FormatMode::ForceSubPatch)) { + value += std::format(".{}", m_SubPatch); + } + + if (!m_PreReleases.empty()) { + if (!noSeparator) { + value += "-"; + } + for (std::size_t i = 0; i < m_PreReleases.size(); ++i) { + value += std::visit( + [shortAlphaBeta](auto const& pre) -> std::string { + if constexpr (std::is_same_v<decltype(pre), ReleaseType const&>) { + switch (pre) { + case Development: + return "dev"; + case Alpha: + return shortAlphaBeta ? "a" : "alpha"; + case Beta: + return shortAlphaBeta ? "b" : "beta"; + case ReleaseCandidate: + return "rc"; + } + return ""; + } else { + return std::to_string(pre); + } + }, + m_PreReleases[i]); + if (!noSeparator && i < m_PreReleases.size() - 1) { + value += "."; + } + } + } + + if (!modes.testFlag(FormatMode::NoMetadata) && !m_BuildMetadata.isEmpty()) { + value += "+" + m_BuildMetadata.toStdString(); + } + + return QString::fromStdString(value); +} + +namespace +{ + // consume the given iterator until the given end iterator or until a non-zero value + // is found + // + template <class It> + It consumePreReleaseZeros(It it, It end) + { + for (; it != end; ++it) { + if (!std::holds_alternative<int>(*it) != 0 || std::get<int>(*it) != 0) { + break; + } + } + return it; + }; +} // namespace + +std::strong_ordering operator<=>(const Version& lhs, const Version& rhs) +{ + auto mmp_cmp = + std::forward_as_tuple(lhs.major(), lhs.minor(), lhs.patch(), lhs.subpatch()) <=> + std::forward_as_tuple(rhs.major(), rhs.minor(), rhs.patch(), rhs.subpatch()); + + // major.minor.patch have precedence over everything else + if (mmp_cmp != std::strong_ordering::equal) { + return mmp_cmp; + } + + // handle cases were one is a pre-release and not the other - the pre-release is + // "less" than the release + if (lhs.isPreRelease() && !rhs.isPreRelease()) { + return std::strong_ordering::less; + } + + if (!lhs.isPreRelease() && rhs.isPreRelease()) { + return std::strong_ordering::greater; + } + + // compare pre-release fields + auto lhsIt = lhs.preReleases().begin(), rhsIt = rhs.preReleases().begin(); + for (; lhsIt != lhs.preReleases().end() && rhsIt != rhs.preReleases().end(); + ++lhsIt, ++rhsIt) { + + const auto &lhsPre = *lhsIt, rhsPre = *rhsIt; + + // if one is alpha/beta/etc. and the other is numeric, the alpha/beta/etc. is lower + // than the numeric one, which matches the index + auto pre_cmp = lhsPre.index() <=> rhsPre.index(); + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + + // compare the actual values + pre_cmp = lhsPre <=> rhsPre; + if (pre_cmp != std::strong_ordering::equal) { + return pre_cmp; + } + } + + // the code below does not follow semver 100% (I think) - basically, this makes stuff + // like 2.4.1rc1.0 equals to 2.4.1rc1, which according to semver is probably not right + // but is probably best for us + // + + // if we land here, we have consumed one of the pre-release, we skip all the 0 in the + // remaining one + lhsIt = consumePreReleaseZeros(lhsIt, lhs.preReleases().end()); + rhsIt = consumePreReleaseZeros(rhsIt, rhs.preReleases().end()); + + const auto lhsConsumed = lhsIt == lhs.preReleases().end(), + rhsConsumed = rhsIt == rhs.preReleases().end(); + + if (lhsConsumed && rhsConsumed) { + return std::strong_ordering::equal; + } else if (!lhsConsumed) { + return std::strong_ordering::greater; + } else { + return std::strong_ordering::less; + } +} + +} // namespace MOBase diff --git a/libs/uibase/src/widgetutility.cpp b/libs/uibase/src/widgetutility.cpp new file mode 100644 index 0000000..e3a4b06 --- /dev/null +++ b/libs/uibase/src/widgetutility.cpp @@ -0,0 +1,59 @@ +#include <uibase/widgetutility.h> +#include <uibase/eventfilter.h> +#include <QCheckBox> +#include <QHeaderView> +#include <QMenu> +#include <QWidgetAction> + +namespace MOBase +{ + +void onHeaderContextMenu(QTreeView* view, const QPoint& pos) +{ + auto* header = view->header(); + + QMenu menu; + + // display a list of all headers as checkboxes + QAbstractItemModel* model = header->model(); + + for (int i = 1; i < model->columnCount(); ++i) { + const QString columnName = model->headerData(i, Qt::Horizontal).toString(); + + auto* checkBox = new QCheckBox(&menu); + checkBox->setText(columnName); + checkBox->setChecked(!header->isSectionHidden(i)); + + // Enable the checkbox if 1) the section is visible, or 2) the + // EnabledColumnRole is not found, or 3) the value for the role is true. + auto display = model->headerData(i, Qt::Horizontal, EnabledColumnRole); + checkBox->setEnabled(!header->isSectionHidden(i) || !display.isValid() || + display.toBool()); + + auto* checkableAction = new QWidgetAction(&menu); + checkableAction->setDefaultWidget(checkBox); + + QObject::connect(checkBox, &QCheckBox::clicked, [=] { + header->setSectionHidden(i, !checkBox->isChecked()); + }); + + menu.addAction(checkableAction); + } + + menu.exec(header->viewport()->mapToGlobal(pos)); +} + +void setCustomizableColumns(QTreeView* view) +{ + auto* header = view->header(); + + header->setSectionsMovable(true); + header->setContextMenuPolicy(Qt::CustomContextMenu); + + QObject::connect(header, &QWidget::customContextMenuRequested, view, + [view](auto&& pos) { + onHeaderContextMenu(view, pos); + }); +} + +} // namespace MOBase |
