From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: Fluorine Manager: full Linux port of Mod Organizer 2 Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 --- libs/installer_omod/src/CMakeLists.txt | 67 ++ libs/installer_omod/src/DummyCSFile.cs | 16 + libs/installer_omod/src/MessageBoxHelper.cpp | 61 ++ libs/installer_omod/src/MessageBoxHelper.h | 37 + libs/installer_omod/src/OMODFrameworkWrapper.cpp | 771 +++++++++++++++++++++ libs/installer_omod/src/OMODFrameworkWrapper.h | 69 ++ libs/installer_omod/src/QObject_unique_ptr.h | 20 + .../src/implementations/CodeProgress.cpp | 85 +++ .../src/implementations/CodeProgress.h | 59 ++ libs/installer_omod/src/implementations/Logger.cpp | 45 ++ libs/installer_omod/src/implementations/Logger.h | 17 + .../src/implementations/ScriptFunctions.cpp | 320 +++++++++ .../src/implementations/ScriptFunctions.h | 122 ++++ libs/installer_omod/src/installerOmod.cpp | 204 ++++++ libs/installer_omod/src/installerOmod.h | 75 ++ libs/installer_omod/src/installer_omod_en.ts | 159 +++++ .../src/interop/QtDotNetConverters.cpp | 17 + .../src/interop/QtDotNetConverters.h | 9 + .../src/interop/StdDotNetConverters.cpp | 35 + .../src/interop/StdDotNetConverters.h | 13 + libs/installer_omod/src/newstuff/namedialog.cpp | 37 + libs/installer_omod/src/newstuff/namedialog.h | 28 + libs/installer_omod/src/newstuff/namedialog.ui | 89 +++ libs/installer_omod/src/newstuff/rtfPopup.cpp | 33 + libs/installer_omod/src/newstuff/rtfPopup.h | 11 + libs/installer_omod/src/oldstuff/DialogSelect.cpp | 301 ++++++++ libs/installer_omod/src/oldstuff/DialogSelect.h | 46 ++ .../src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE | 21 + .../MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp | 165 +++++ .../MIT-licencedCodeToDoStuff/checkboxwordwrap.h | 75 ++ .../MIT-licencedCodeToDoStuff/clickablelabel.cpp | 24 + .../MIT-licencedCodeToDoStuff/clickablelabel.h | 27 + libs/installer_omod/src/oldstuff/folder.md | 1 + 33 files changed, 3059 insertions(+) create mode 100644 libs/installer_omod/src/CMakeLists.txt create mode 100644 libs/installer_omod/src/DummyCSFile.cs create mode 100644 libs/installer_omod/src/MessageBoxHelper.cpp create mode 100644 libs/installer_omod/src/MessageBoxHelper.h create mode 100644 libs/installer_omod/src/OMODFrameworkWrapper.cpp create mode 100644 libs/installer_omod/src/OMODFrameworkWrapper.h create mode 100644 libs/installer_omod/src/QObject_unique_ptr.h create mode 100644 libs/installer_omod/src/implementations/CodeProgress.cpp create mode 100644 libs/installer_omod/src/implementations/CodeProgress.h create mode 100644 libs/installer_omod/src/implementations/Logger.cpp create mode 100644 libs/installer_omod/src/implementations/Logger.h create mode 100644 libs/installer_omod/src/implementations/ScriptFunctions.cpp create mode 100644 libs/installer_omod/src/implementations/ScriptFunctions.h create mode 100644 libs/installer_omod/src/installerOmod.cpp create mode 100644 libs/installer_omod/src/installerOmod.h create mode 100644 libs/installer_omod/src/installer_omod_en.ts create mode 100644 libs/installer_omod/src/interop/QtDotNetConverters.cpp create mode 100644 libs/installer_omod/src/interop/QtDotNetConverters.h create mode 100644 libs/installer_omod/src/interop/StdDotNetConverters.cpp create mode 100644 libs/installer_omod/src/interop/StdDotNetConverters.h create mode 100644 libs/installer_omod/src/newstuff/namedialog.cpp create mode 100644 libs/installer_omod/src/newstuff/namedialog.h create mode 100644 libs/installer_omod/src/newstuff/namedialog.ui create mode 100644 libs/installer_omod/src/newstuff/rtfPopup.cpp create mode 100644 libs/installer_omod/src/newstuff/rtfPopup.h create mode 100644 libs/installer_omod/src/oldstuff/DialogSelect.cpp create mode 100644 libs/installer_omod/src/oldstuff/DialogSelect.h create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp create mode 100644 libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h create mode 100644 libs/installer_omod/src/oldstuff/folder.md (limited to 'libs/installer_omod/src') diff --git a/libs/installer_omod/src/CMakeLists.txt b/libs/installer_omod/src/CMakeLists.txt new file mode 100644 index 0000000..f28851c --- /dev/null +++ b/libs/installer_omod/src/CMakeLists.txt @@ -0,0 +1,67 @@ +# 3.23 for CMAKE_VS_NUGET_PACKAGE_RESTORE +cmake_minimum_required(VERSION 3.23) + +find_package(mo2-cmake CONFIG REQUIRED) +find_package(mo2-uibase CONFIG REQUIRED) + +# Dummy .NET library as VS_PACKAGE_REFERENCES doesn't work on C++/CLI projects yet +# Needs to be declared before cmake_common stuff is included as that polutes the environment and makes C# get compiled as C++ +add_library(dummy_cs_project SHARED DummyCSFile.cs) +set_target_properties(dummy_cs_project PROPERTIES + LINKER_LANGUAGE CSharp + VS_PACKAGE_REFERENCES "OMODFramework_2.2.2;OMODFramework.Scripting_2.2.2;RtfPipe_1.0.7388.1242" +) + +add_library(installer_omod SHARED) +mo2_configure_plugin(installer_omod WARNINGS OFF CLI ON) +target_link_libraries(installer_omod PRIVATE mo2::uibase) + +# I'd like to use get_target_property(source_files ${PROJECT_NAME} SOURCES) as +# globbing is naughty, but need to filter out the things that aren't relative to this directory. +file(GLOB_RECURSE source_files CONFIGURE_DEPENDS *.cpp;*.h;*.ui) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX src FILES ${source_files}) + +# Ideally we'd use "$", but the relevant property +# doesn't support generator expressions. +set(omod_framework_prefix "${CMAKE_BINARY_DIR}/$(Configuration)" + CACHE PATH "Path where OMODFramework.dll and OMODFramework.Scripting.dll can be found. The default value is where the nuget package artifacts will end up. Override if you want to use a custom build.") + +set_target_properties(${PROJECT_NAME} PROPERTIES + COMMON_LANGUAGE_RUNTIME "" + # latest may not work with CLR + CXX_STANDARD 20 + #DOTNET_TARGET_FRAMEWORK "netstandard2.0" + VS_DOTNET_REFERENCE_OMODFramework "${omod_framework_prefix}/OMODFramework.dll" + VS_DOTNET_REFERENCE_OMODFramework.Scripting "${omod_framework_prefix}/OMODFramework.Scripting.dll" + VS_DOTNET_REFERENCE_RtfPipe "${CMAKE_BINARY_DIR}/$(Configuration)/RtfPipe.dll" + VS_DOTNET_REFERENCES "System.dll;System.Core.dll" +) + +target_compile_options( + installer_omod + PRIVATE + # OMODFramework and OMODFramework.Scripting reference different .NET standard libraries. This generates warnings when using them together. + "/wd4691") + +# We don't want ERROR defined +target_compile_definitions(installer_omod PRIVATE "NOGDI") + +# We don't need to actually link with dummy_cs_project, especially as its dependencies +# aren't pulled in. We do need it to build first, though. +add_dependencies(installer_omod dummy_cs_project) + +mo2_install_plugin(installer_omod FOLDER) + +install( + FILES + "$/OMODFramework.dll" + "$/OMODFramework.Scripting.dll" + "$/ICSharpCode.SharpZipLib.dll" + "$/System.Drawing.Common.dll" + "$/RtfPipe.dll" + DESTINATION bin/plugins/installer_omod/ +) +install( + FILES "$/ICSharpCode.SharpZipLib.pdb" + DESTINATION pdb) +# Other PDB files get downloaded by Visual Studio during debugging when https://symbols.nuget.org/download/symbols is in the symbol server list diff --git a/libs/installer_omod/src/DummyCSFile.cs b/libs/installer_omod/src/DummyCSFile.cs new file mode 100644 index 0000000..b093f32 --- /dev/null +++ b/libs/installer_omod/src/DummyCSFile.cs @@ -0,0 +1,16 @@ +using OMODFramework; +using OMODFramework.Scripting; +using RtfPipe; + +namespace Dummy +{ + class Stub + { + static void Stub2() + { + OMOD omod = null; + IScriptFunctions scriptFunctions = null; + Rtf.ToHtml(""); + } + } +} diff --git a/libs/installer_omod/src/MessageBoxHelper.cpp b/libs/installer_omod/src/MessageBoxHelper.cpp new file mode 100644 index 0000000..29f0be3 --- /dev/null +++ b/libs/installer_omod/src/MessageBoxHelper.cpp @@ -0,0 +1,61 @@ +#include "MessageBoxHelper.h" + +#include + +MessageBoxHelper::MessageBoxHelper() +{ + moveToThread(QApplication::instance()->thread()); + + connect(this, &MessageBoxHelper::criticalMessageBoxSignal, this, &MessageBoxHelper::criticalMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::informationMessageBoxSignal, this, &MessageBoxHelper::informationMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::questionMessageBoxSignal, this, &MessageBoxHelper::questionMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &MessageBoxHelper::warningMessageBoxSignal, this, &MessageBoxHelper::warningMessageBoxSlot, Qt::ConnectionType::BlockingQueuedConnection); +} + +QMessageBox::StandardButton MessageBoxHelper::critical(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit criticalMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::information(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit informationMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit questionMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +QMessageBox::StandardButton MessageBoxHelper::warning(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + QMessageBox::StandardButton response; + emit warningMessageBoxSignal(response, parent, title, text, buttons, defaultButton); + return response; +} + +void MessageBoxHelper::criticalMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::critical(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::informationMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::information(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::questionMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::question(parent, title, text, buttons, defaultButton); +} + +void MessageBoxHelper::warningMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) +{ + standardButtonOut = QMessageBox::warning(parent, title, text, buttons, defaultButton); +} diff --git a/libs/installer_omod/src/MessageBoxHelper.h b/libs/installer_omod/src/MessageBoxHelper.h new file mode 100644 index 0000000..aa6b926 --- /dev/null +++ b/libs/installer_omod/src/MessageBoxHelper.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +class MessageBoxHelper : public QObject +{ + Q_OBJECT + +public: + MessageBoxHelper(); + + QMessageBox::StandardButton critical(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton information(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + QMessageBox::StandardButton warning(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + +signals: + void criticalMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void informationMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void questionMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + + void warningMessageBoxSignal(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); + +private slots: + void criticalMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void informationMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void questionMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); + + void warningMessageBoxSlot(QMessageBox::StandardButton& standardButtonOut, QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton); +}; diff --git a/libs/installer_omod/src/OMODFrameworkWrapper.cpp b/libs/installer_omod/src/OMODFrameworkWrapper.cpp new file mode 100644 index 0000000..0a13620 --- /dev/null +++ b/libs/installer_omod/src/OMODFrameworkWrapper.cpp @@ -0,0 +1,771 @@ +#include "OMODFrameworkWrapper.h" + +using namespace cli; + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "implementations/CodeProgress.h" +#include "implementations/Logger.h" +#include "implementations/ScriptFunctions.h" + +#include "interop/QtDotNetConverters.h" +#include "interop/StdDotNetConverters.h" + +#include "newstuff/namedialog.h" +#include "newstuff/rtfPopup.h" + +#include "MessageBoxHelper.h" + +namespace fs = std::filesystem; + +// retrieve the path to the folder containing the proxy DLL +fs::path getPluginFolder() +{ + wchar_t path[MAX_PATH]; + HMODULE hm = NULL; + + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCWSTR)&getPluginFolder, &hm) == 0) + { + return {}; + } + if (GetModuleFileName(hm, path, sizeof(path)) == 0) + { + return {}; + } + + return fs::path(path).parent_path(); +} + +// We want to search the plugin data directory for .NET DLLs +class AssemblyResolver +{ +public: + static bool getInitialised() { return sInitialised; } + + static void initialise(MOBase::IOrganizer* organizer) + { + if (sInitialised) + return; + sPluginDataPath = getPluginFolder(); + System::AppDomain::CurrentDomain->AssemblyResolve += gcnew System::ResolveEventHandler(&OnAssemblyResolve); + sInitialised = true; + } + +private: + static System::Reflection::Assembly^ OnAssemblyResolve(System::Object^ sender, System::ResolveEventArgs^ args); + + static bool sInitialised; + static QDir sPluginDataPath; +}; + +bool AssemblyResolver::sInitialised = false; +QDir AssemblyResolver::sPluginDataPath; + +System::Reflection::Assembly^ AssemblyResolver::OnAssemblyResolve(System::Object^ sender, System::ResolveEventArgs^ args) +{ + QString name = toQString(args->Name).section(',', 0, 0) + ".dll"; + if (sPluginDataPath.exists(name)) + return System::Reflection::Assembly::LoadFrom(toDotNetString(sPluginDataPath.absoluteFilePath(name))); + return nullptr; +} + +OMODFrameworkWrapper::OMODFrameworkWrapper(MOBase::IOrganizer* organizer, QWidget* parentWidget) + : mMoInfo(organizer) + , mParentWidget(parentWidget) + , mWaitDialog(make_nullptr()) +{ + try + { + AssemblyResolver::initialise(mMoInfo); + + constructorHelper(); + + connect(this, &OMODFrameworkWrapper::pickModName, this, &OMODFrameworkWrapper::pickModNameSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::createMod, this, &OMODFrameworkWrapper::createModSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::displayReadme, this, &OMODFrameworkWrapper::displayReadmeSlot, Qt::ConnectionType::BlockingQueuedConnection); + connect(this, &OMODFrameworkWrapper::showWaitDialog, this, &OMODFrameworkWrapper::showWaitDialogSlot, Qt::ConnectionType::QueuedConnection); + connect(this, &OMODFrameworkWrapper::hideWaitDialog, this, &OMODFrameworkWrapper::hideWaitDialogSlot, Qt::ConnectionType::QueuedConnection); + + initFrameworkSettings(); + } + catch (const std::exception& e) + { + throw; + } + catch (System::Exception^ dotNetException) + { + throw toStdException(dotNetException); + } +} + +void OMODFrameworkWrapper::constructorHelper() +{ + // We can't call a function doing this before AssemblyResolver::initialise happens as the DLL needs to be available before its stack frame is created. + mTempPathStack.push(toQString(OMODFramework::Framework::Settings->TempPath)); +} + +ref class InstallInAnotherThreadHelper +{ +public: + InstallInAnotherThreadHelper(OMODFrameworkWrapper* owner, MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) + : mModName(&modName) + , mGameName(&gameName) + , mArchiveName(&archiveName) + , mVersion(&version) + , mNexusID(nexusID) + , mOwner(owner) + , mExceptionPtr(new std::exception_ptr) + , mHasResult(false) + {} + + ~InstallInAnotherThreadHelper() + { + if (!mExceptionPtr) + return; + + this->!InstallInAnotherThreadHelper(); + } + + !InstallInAnotherThreadHelper() + { + delete mExceptionPtr; + mExceptionPtr = nullptr; + } + + void Run() + { + try + { + mResult = mOwner->install(*mModName, *mGameName, *mArchiveName, *mVersion, mNexusID); + mHasResult = true; + } + catch (...) + { + *mExceptionPtr = std::current_exception(); + } + } + + bool HasResult() + { + return mHasResult; + } + + OMODFrameworkWrapper::EInstallResult Result() + { + return mResult; + } + + std::exception_ptr ExceptionPtr() + { + return *mExceptionPtr; + } + +private: + // We own none of these pointers. They're pointers because managed objects can't have unmanaged members, but a pointer is just an integer of some form, which is the same. + MOBase::GuessedValue* mModName; + QString* mGameName; + const QString* mArchiveName; + const QString* mVersion; + int mNexusID; + + OMODFrameworkWrapper* mOwner; + + std::exception_ptr* mExceptionPtr; + OMODFrameworkWrapper::EInstallResult mResult; + bool mHasResult; +}; + +OMODFrameworkWrapper::EInstallResult OMODFrameworkWrapper::installInAnotherThread(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + QEventLoop eventLoop; + InstallInAnotherThreadHelper^ helper = gcnew InstallInAnotherThreadHelper(this, modName, gameName, archiveName, version, nexusID); + System::Threading::Tasks::Task^ installationTask = System::Threading::Tasks::Task::Run(gcnew System::Action(helper, &InstallInAnotherThreadHelper::Run)); + + // TODO: connect stuff to eventLoop.wakeUp. + // Installation manager does this with futureWatcher.finished and progressUpdate + while (!installationTask->IsCompleted) + eventLoop.processEvents(QEventLoop::AllEvents | QEventLoop::WaitForMoreEvents); + + if (helper->HasResult()) + return helper->Result(); + else if (helper->ExceptionPtr()) + std::rethrow_exception(helper->ExceptionPtr()); + else + throw std::runtime_error("Something went horribly wrong when asynchronously installing an OMOD. We don't even have the original exception."); +} + +// TODO: replace with std::scope_exit when it leaves std::experimental +template +class scope_guard +{ +public: + scope_guard(T onExit) : mOnExit(onExit) {} + ~scope_guard() { mOnExit(); } +private: + T mOnExit; +}; + +OMODFrameworkWrapper::EInstallResult OMODFrameworkWrapper::install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + try + { + QObject_unique_ptr messageBoxHelper = make_unique(); + + MOBase::log::debug("Installing {} as OMOD", archiveName); + + emit showWaitDialog("Initializing OMOD installer... "); + refreshFrameworkSettings(); + // Stack allocating should dispose like a `using` statement in C# + OMODFramework::OMOD omod(toDotNetString(archiveName)); + emit hideWaitDialog(); + + if (!System::String::IsNullOrEmpty(omod.ModName)) + modName.update(toQString(omod.ModName), MOBase::EGuessQuality::GUESS_META); + + bool nameNotCancelled; + emit pickModName(nameNotCancelled, modName); + if (!nameNotCancelled) + return EInstallResult::RESULT_CANCELED; + + MOBase::IModInterface* modInterface; + emit createMod(modInterface, modName); + if (!modInterface) + return EInstallResult::RESULT_CANCELED; + + { + QTemporaryDir tempPath(modInterface->absolutePath() + "/OMODInstallTempXXXXXX"); + pushTempPath(tempPath.path()); + scope_guard tempPathGuard([this]() { this->popTempPath(); }); + + if (omod.HasReadme) + emit displayReadme(toQString(omod.ModName), toQString(omod.GetReadme())); + + if (omod.HasScript) + { + MOBase::log::debug("Mod has script. Run it."); + OMODFramework::Scripting::IScriptFunctions^ scriptFunctions = gcnew ScriptFunctions(mParentWidget, mMoInfo); + + System::Collections::Generic::List^ activeBSAs = gcnew System::Collections::Generic::List(); + { + // Hopefully some other part of MO2 also knows this and this can be refactored + auto dataArchives = mMoInfo->gameFeatures()->gameFeature(); + // force-enabled by engine + auto bsas = dataArchives->vanillaArchives(); + // explicitly enabled in INI. For Oblivion, excludes the vanilla BSAs by default. + bsas.append(dataArchives->archives(mMoInfo->profile().get())); + for (const auto& bsa : bsas) + { + QString path = mMoInfo->resolvePath(bsa); + if (!path.isEmpty()) + activeBSAs->Add(toDotNetString(path)); + } + + std::map loadOrder; + for (const auto& plugin : mMoInfo->pluginList()->pluginNames()) + { + int loadIndex = mMoInfo->pluginList()->loadOrder(plugin); + if (loadIndex != -1) + loadOrder[loadIndex] = plugin; + } + for (const auto& [loadIndex, plugin] : loadOrder) + { + QString bsaPath = QFileInfo(plugin).completeBaseName() + ".bsa"; + bsaPath = mMoInfo->resolvePath(bsaPath); + if (!bsaPath.isEmpty()) + activeBSAs->Add(toDotNetString(bsaPath)); + } + } + OMODFramework::Framework::LoadBSAs(activeBSAs); + scope_guard bsaGuard([]() { OMODFramework::Framework::ClearBSAs(); }); + + System::String^ dataPath = omod.GetDataFiles(); + System::String^ pluginsPath = omod.GetPlugins(); + OMODFramework::ScriptReturnData^ scriptData = OMODFramework::Scripting::ScriptRunner::RunScript(%omod, scriptFunctions, dataPath, pluginsPath); + if (!scriptData) + throw std::runtime_error("OMOD script returned no result. This isn't supposed to happen."); + if (scriptData->CancelInstall) + return EInstallResult::RESULT_CANCELED; + + // inis first so that you don't need to wait for extraction before a second batch of questions appears + if (scriptData->INIEdits && scriptData->INIEdits->Count) + { + QMap> iniEdits; + for each (OMODFramework::INIEditInfo ^ edit in scriptData->INIEdits) + iniEdits[toQString(edit->Section)][toQString(edit->Name)] = toQString(edit->NewValue); + // This feels like something I shouldn't need to do manually + QVariantMap iniEditsVariants; + for (const auto& section : iniEdits.keys()) + { + QVariantMap innerMap; + for (const auto& setting : iniEdits[section]) + innerMap[setting] = iniEdits[section][setting]; + iniEditsVariants[section] = innerMap; + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".iniEdits", iniEditsVariants); + + QString oblivionIniPath = mMoInfo->profile()->absoluteIniFilePath("Oblivion.ini"); + bool yesToAll = false; + for each (OMODFramework::INIEditInfo ^ edit in scriptData->INIEdits) + { + QString section = toQString(edit->Section); + section = section.mid(1, section.size() - 2); + QString name = toQString(edit->Name); + QString newValue = toQString(edit->NewValue); + QString oldValue; + if (edit->OldValue) + oldValue = toQString(edit->OldValue); + else + { + // I'm pretty sure this is the maximum length for vanilla Oblivion. + wchar_t buffer[256]; + if (GetPrivateProfileString(section.toStdWString().data(), name.toStdWString().data(), nullptr, buffer, sizeof(buffer) / sizeof(buffer[0]), oblivionIniPath.toStdWString().data())) + oldValue = QString::fromWCharArray(buffer); + } + + MOBase::log::debug("OMOD wants to set [{}] {} to \"{}\", was \"{}\"", section, name, newValue, oldValue); + + if (oldValue == newValue) + { + MOBase::log::debug("Value is unchanged, not nagging user."); + continue; + } + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (!oldValue.isEmpty()) + { + /*: %1 is the mod name + [%2] is the ini section name. + %3 is the ini setting name. + %4 is the value already in Oblivion.ini. + %5 is the value the mod wants to set. + */ + message = tr("%1 wants to change [%2] %3 from \"%4\" to \"%5\"").arg(modName).arg(section).arg(name).arg(oldValue).arg(newValue); + } + else + { + /*: %1 is the mod name + [%2] is the ini section name. + %3 is the ini setting name. + %5 is the value the mod wants to set. + */ + message = tr("%1 wants to set [%2] %3 to \"%4\"").arg(modName).arg(section).arg(name).arg(newValue); + } + + response = messageBoxHelper->question(mParentWidget, tr("Update INI?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Doing edit."); + MOBase::WriteRegistryValue(section.toStdWString().data(), name.toStdWString().data(), newValue.toStdWString().data(), oblivionIniPath.toStdWString().data()); + } + else + MOBase::log::debug("User skipped edit."); + } + } + + scriptData->Pretty(%omod, dataPath, pluginsPath); + // no compatability between auto and var makes me :angery: + System::Collections::Generic::HashSet^ installedPlugins = gcnew System::Collections::Generic::HashSet(System::StringComparer::InvariantCultureIgnoreCase); + for each (OMODFramework::InstallFile file in scriptData->InstallFiles) + { + System::String^ destinationPath = System::IO::Path::Combine(toDotNetString(modInterface->absolutePath()), file.InstallTo); + System::IO::Directory::CreateDirectory(System::IO::Path::GetDirectoryName(destinationPath)); + System::IO::File::Copy(file.InstallFrom, destinationPath, true); + System::String^ extension = System::IO::Path::GetExtension(file.InstallTo); + if (extension && (extension->Equals(".esm", System::StringComparison::InvariantCultureIgnoreCase) || extension->Equals(".esp", System::StringComparison::InvariantCultureIgnoreCase))) + installedPlugins->Add(file.InstallTo); + } + + if (scriptData->UncheckedPlugins) + installedPlugins->ExceptWith(scriptData->UncheckedPlugins); + + QStringList defaultActivePlugins; + for each (System::String ^ plugin in installedPlugins) + defaultActivePlugins.append(toQString(plugin)); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultActivePlugins", defaultActivePlugins); + QStringList defaultInactivePlugins; + if (scriptData->UncheckedPlugins) + { + for each (System::String ^ plugin in scriptData->UncheckedPlugins) + defaultInactivePlugins.append(toQString(plugin)); + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultInactivePlugins", defaultInactivePlugins); + + QStringList registeredBSAs; + if (scriptData->RegisterBSASet) + { + for each (System::String ^ bsa in scriptData->RegisterBSASet) + registeredBSAs.append(toQString(bsa)); + } + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".registeredBSAs", registeredBSAs); + + if (scriptData->SDPEdits && scriptData->SDPEdits->Count) + { + for each (OMODFramework::SDPEditInfo ^ shaderEdit in scriptData->SDPEdits) + { + System::String^ destinationPath = System::IO::Path::Combine(toDotNetString(modInterface->absolutePath()), "Shaders", "OMOD", toDotNetString(QString::number(shaderEdit->Package))); + System::IO::Directory::CreateDirectory(destinationPath); + System::IO::File::Copy(shaderEdit->BinaryObject, System::IO::Path::Combine(destinationPath, shaderEdit->Shader), true); + } + } + + std::map unhandledScriptReturnDataCounts; + // This is a mapping from plugin name to an enum saying whether OBMM should allow the user to deactivate an ESP from the OMOD, disallow it, or just warn. By default, it'd warn. + unhandledScriptReturnDataCounts["ESPDeactivation"] = scriptData->ESPDeactivation ? scriptData->ESPDeactivation->Count : 0; + // There's nothing in the OBMM documentation claiming the function that sets this exists. + unhandledScriptReturnDataCounts["EarlyPlugins"] = scriptData->EarlyPlugins ? scriptData->EarlyPlugins->Count : 0; + // Sets load order a.esp, b.esp, true and b.esp, a.esp, false both mean the same thing. + unhandledScriptReturnDataCounts["LoadOrderSet"] = scriptData->LoadOrderSet ? scriptData->LoadOrderSet->Count : 0; + // Says this OMOD conflicts with another, potentially with a description. + unhandledScriptReturnDataCounts["ConflictsWith"] = scriptData->ConflictsWith ? scriptData->ConflictsWith->Count : 0; + // Says this OMOD depends on another, potentially with a description. + unhandledScriptReturnDataCounts["DependsOn"] = scriptData->DependsOn ? scriptData->DependsOn->Count : 0; + + // Contains a list of GMSTs and Globals to edit in the mod's ESPs. May a higher being have mercy on us if anyone ever used this. + unhandledScriptReturnDataCounts["ESPEdits"] = scriptData->ESPEdits ? scriptData->ESPEdits->Count : 0; + // OMODFramework is handling this for us, so don't sweat it. + unhandledScriptReturnDataCounts["PatchFiles"] = scriptData->PatchFiles ? scriptData->PatchFiles->Count : 0; + + for (const auto& [name, usageCount] : unhandledScriptReturnDataCounts) + { + if (usageCount) + { + /*: %1 is the mod name + %2 is the name of a field in the OMOD's return data + Hopefully this message will never be seen by anyone, but if it is, they need to know to tell the Mod Organizer 2 dev team. + */ + QString userMessage = tr("%1 has data for %2, but Mod Organizer 2 doesn't know what to do with it yet. Please report this to the Mod Organizer 2 development team (ideally by sending us your interface log) as we didn't find any OMODs that actually did this, and we need to know that they exist."); + userMessage = userMessage.arg(toQString(omod.ModName)); + userMessage = userMessage.arg(name); + messageBoxHelper->warning(mParentWidget, tr("Mod Organizer 2 can't completely install this OMOD."), userMessage); + MOBase::log::warn("{} ({}) contains {} entries for {}", toUTF8String(omod.ModName), archiveName, usageCount, name); + } + } + } + else + { + MOBase::log::debug("Mod has no script. Install contents directly."); + QString data = toQString(omod.GetDataFiles()); + QString plugins = toQString(omod.GetPlugins()); + if (!data.isNull()) + { + if (MOBase::shellMove(data + "/*.*", modInterface->absolutePath(), true, mParentWidget)) + MOBase::log::debug("Installed mod files."); + else + MOBase::log::error("Error while installing mod files."); + QFile::remove(data); + } + if (!plugins.isNull()) + { + QStringList defaultActivePlugins = QDir(plugins).entryList({ "*.esm", "*.esp" }, QDir::Files); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultActivePlugins", defaultActivePlugins); + modInterface->setPluginSetting("Omod Installer", toQString(omod.ModName) + ".defaultInactivePlugins", QStringList()); + if (MOBase::shellMove(plugins + "/*.*", modInterface->absolutePath(), true, mParentWidget)) + MOBase::log::debug("Installed mod plugins."); + else + MOBase::log::error("Error while installing mod plugins."); + QFile::remove(plugins); + } + } + + // on success, set mod info + MOBase::VersionInfo modVersion(std::max(int(omod.MajorVersion), 0), std::max(int(omod.MinorVersion), 0), std::max(int(omod.BuildVersion), 0)); + modInterface->setVersion(modVersion); + + // TODO: parse omod.Website. If it's Nexus, set the ID, otherwise set custom URL in meta.ini. We can't set the URL with the installation manager. + // TODO: maybe convert omod.Description to HTML and set it as nexusDescription + // TODO: maybe Holt will finish the proposed mod metadata API and there'll be a better, tidier option. + if (nexusID != -1) + modInterface->setNexusID(nexusID); + + modInterface->setInstallationFile(archiveName); + } + + OMODFramework::Framework::CleanTempDir(false); + + QStringList omodsPendingPostInstall = modInterface->pluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()).toStringList(); + if (!omodsPendingPostInstall.contains(toQString(omod.ModName))) + { + omodsPendingPostInstall.append(toQString(omod.ModName)); + modInterface->setPluginSetting("Omod Installer", "omodsPendingPostInstall", omodsPendingPostInstall); + } + return EInstallResult::RESULT_SUCCESS; + } + catch (const std::exception& e) + { + throw; + } + catch (System::Exception^ dotNetException) + { + throw toStdException(dotNetException); + } +} + +void OMODFrameworkWrapper::setParentWidget(QWidget* parentWidget) +{ + mParentWidget = parentWidget; + if (OMODFramework::Framework::Settings->CodeProgress) + static_cast(OMODFramework::Framework::Settings->CodeProgress)->setParentWidget(mParentWidget); +} + +const std::array pluginStateNames = { "STATE_MISSING", "STATE_INACTIVE", "STATE_ACTIVE" }; + +void OMODFrameworkWrapper::onInstallationEnd(EInstallResult status, MOBase::IModInterface* mod) +{ + if (status != EInstallResult::RESULT_SUCCESS || !mod) + return; + + QStringList omodsPendingPostInstall = mod->pluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()).toStringList(); + + if (!omodsPendingPostInstall.empty() && !(mMoInfo->modList()->state(mod->name()) & MOBase::IModList::STATE_ACTIVE)) + { + auto response = QMessageBox::question(mParentWidget, tr("Activate mod?"), + /*: %1 is the left-pane mod name. + %2 is the name from the metadata of an OMOD. + */ + tr("%1 contains the OMOD %2. OMODs may have post-installation actions like activating ESPs. Would you like to enable the mod so this can happen now?").arg(mod->name()).arg(omodsPendingPostInstall[0]), + QMessageBox::Yes | QMessageBox::No); + + if (response == QMessageBox::StandardButton::Yes) + mMoInfo->modList()->setActive(mod->name(), true); + else + return; + } + + for (const auto& omodName : omodsPendingPostInstall) + { + MOBase::log::debug("Running post-install for {}", omodName); + QStringList defaultActivePlugins = mod->pluginSetting("Omod Installer", omodName + ".defaultActivePlugins", QStringList()).toStringList(); + QStringList defaultInactivePlugins = mod->pluginSetting("Omod Installer", omodName + ".defaultInactivePlugins", QStringList()).toStringList(); + + bool yesToAll = false; + for (const auto& plugin : defaultActivePlugins) + { + MOBase::IPluginList::PluginStates oldState = mMoInfo->pluginList()->state(plugin); + MOBase::log::debug("OMOD wants to activate {}, was {}", plugin, pluginStateNames[oldState]); + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (oldState == MOBase::IPluginList::STATE_INACTIVE) + { + /*: %1 is the mod name. + %2 is the plugin name. + */ + message = tr("%1 wants to activate %2. Do you want to do so?").arg(omodName).arg(plugin); + + response = QMessageBox::question(mParentWidget, tr("Activate plugin?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + else + { + if (oldState == MOBase::IPluginList::STATE_MISSING) + QMessageBox::warning(mParentWidget, tr("OMOD wants to activate missing plugin"), tr("An OMOD wants to activate a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.")); + continue; + } + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Activating plugin."); + mMoInfo->pluginList()->setState(plugin, MOBase::IPluginList::STATE_ACTIVE); + } + else + MOBase::log::debug("User skipped plugin."); + } + + yesToAll = false; + for (const auto& plugin : defaultInactivePlugins) + { + MOBase::IPluginList::PluginStates oldState = mMoInfo->pluginList()->state(plugin); + MOBase::log::debug("OMOD installed {}, but didn't try and activate it. State was {}", plugin, pluginStateNames[oldState]); + + QMessageBox::StandardButton response; + if (!yesToAll) + { + QString message; + if (oldState == MOBase::IPluginList::STATE_INACTIVE) + { + /*: %1 is the mod name. + %2 is the plugin name. + */ + message = tr("%1 installed %2, but doesn't activate it by default. Do you want to activate it anyway?").arg(omodName).arg(plugin); + + response = QMessageBox::question(mParentWidget, tr("Activate plugin?"), message, QMessageBox::Yes | QMessageBox::No | QMessageBox::YesToAll | QMessageBox::NoToAll); + if (response == QMessageBox::NoToAll) + { + MOBase::log::debug("User skipped all."); + break; + } + + yesToAll |= response == QMessageBox::YesToAll; + } + else + { + if (oldState == MOBase::IPluginList::STATE_MISSING) + QMessageBox::warning(mParentWidget, tr("OMOD claimed to install missing plugin"), tr("An OMOD has activation settings for a missing plugin. This shouldn't be possible. Please report this to a MO2 developer.")); + continue; + } + } + + if (yesToAll || response == QMessageBox::StandardButton::Yes) + { + MOBase::log::debug("Activating plugin."); + mMoInfo->pluginList()->setState(plugin, MOBase::IPluginList::STATE_ACTIVE); + } + else + MOBase::log::debug("User skipped plugin."); + } + + // this is still ugly. + mMoInfo->gameFeatures()->gameFeature()->writePluginLists(mMoInfo->pluginList()); + + + QStringList registeredBSAs = mod->pluginSetting("Omod Installer", omodName + ".registeredBSAs", QStringList()).toStringList(); + if (!registeredBSAs.empty()) + { + MOBase::log::debug("OMOD wants to register BSAs. We can't do that."); + QMessageBox::warning(mParentWidget, tr("Register BSAs"), + /*: %1 is the OMOD name +
  • %2
becomes a list of BSA files + */ + tr("%1 wants to register the following BSA archives, but Mod Organizer 2 can't do that yet due to technical limitations:
  • %2
For now, your options include adding the BSA names to sResourceArchiveList in the game INI, creating a dummy ESP with the same name, or extracting the BSA, all of which have drawbacks.") + .arg(omodName).arg(registeredBSAs.join("
  • ")) + ); + } + } + + mod->setPluginSetting("Omod Installer", "omodsPendingPostInstall", QStringList()); +} + +void OMODFrameworkWrapper::initFrameworkSettings() +{ + OMODFramework::Framework::Settings->CodeProgress = gcnew CodeProgress(mParentWidget); + + // This is a hack to fix an OMOD framework bug and should be removed once it's fixed. + OMODFramework::Framework::Settings->DllPath = System::IO::Path::Combine(System::IO::Path::GetDirectoryName(OMODFramework::Framework::Settings->DllPath), "OMODFramework.Scripting.dll"); + + OMODFramework::LoggingSettings^ loggingSettings = OMODFramework::Framework::Settings->LoggingSettings; + loggingSettings->LogToFile = false; + loggingSettings->LowestLoggingLevel = Logger::OMODLoggingLevel(MOBase::log::getDefault().level()); + loggingSettings->Logger = gcnew Logger(); + loggingSettings->UseLogger = true; + + OMODFramework::ScriptExecutionSettings^ scriptSettings = gcnew OMODFramework::ScriptExecutionSettings(); + scriptSettings->EnableWarnings = true; + scriptSettings->ReadINIWithInterface = false; + scriptSettings->ReadRendererInfoWithInterface = false; + scriptSettings->HandleBSAsWithInterface = false; + scriptSettings->PatchWithInterface = false; + scriptSettings->UseSafePatching = true; + + OMODFramework::Framework::Settings->ScriptExecutionSettings = scriptSettings; +} + +void OMODFrameworkWrapper::refreshFrameworkSettings() +{ + OMODFramework::ScriptExecutionSettings^ scriptSettings = OMODFramework::Framework::Settings->ScriptExecutionSettings; + + if (scriptSettings && mMoInfo->managedGame()) + { + // the managed game isn't set during initFrameworkSettings, so only do this here + scriptSettings->OblivionGamePath = toDotNetString(mMoInfo->managedGame()->gameDirectory().path()); + scriptSettings->OblivionINIPath = toDotNetString(mMoInfo->profile()->absoluteIniFilePath("Oblivion.ini")); + scriptSettings->OblivionRendererInfoPath = System::IO::Path::Combine(toDotNetString(mMoInfo->managedGame()->documentsDirectory().path()), "RendererInfo.txt"); + } +} + +void OMODFrameworkWrapper::pushTempPath(const QString& tempPath) +{ + if (!tempPath.isEmpty()) + OMODFramework::Framework::Settings->TempPath = toDotNetString(tempPath); + mTempPathStack.push(tempPath); +} + +void OMODFrameworkWrapper::popTempPath() +{ + if (mTempPathStack.count() >= 2) + mTempPathStack.pop(); + OMODFramework::Framework::Settings->TempPath = toDotNetString(mTempPathStack.top()); +} + +void OMODFrameworkWrapper::pickModNameSlot(bool& successOut, MOBase::GuessedValue& modName) +{ + NameDialog nameDialog(modName, mParentWidget); + successOut = nameDialog.exec() == QDialog::Accepted; + if (successOut) + modName.update(nameDialog.getName(), MOBase::EGuessQuality::GUESS_USER); +} + +void OMODFrameworkWrapper::createModSlot(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue& modName) +{ + modInterfaceOut = mMoInfo->createMod(modName); +} + +void OMODFrameworkWrapper::displayReadmeSlot(const QString& modName, const QString& readme) +{ + if (QMessageBox::question(mParentWidget, tr("Display Readme?"), + //:
    is a line break. Translators can remove it if it makes things clearer. + tr("The Readme may explain installation options. Display it?
    It will remain visible until you close it.")) == QMessageBox::StandardButton::Yes) + { + // TODO: ideally this wouldn't be part of the same window heirarchy so that modal popups in the installer don't prevent it being moved/resized etc. + // DarNified UI's popups are modal for the whole process, so any fancy trick needs to be *here*. + RtfPopup* readmePopup = new RtfPopup(toDotNetString(readme), mParentWidget); + //: %1 is the mod name + readmePopup->setWindowTitle(tr("%1 Readme").arg(modName)); + readmePopup->show(); + readmePopup->setAttribute(Qt::WA_DeleteOnClose); + } +} + +void OMODFrameworkWrapper::showWaitDialogSlot(QString message) { + mWaitDialog.reset(new QProgressDialog(message, QString(), 0, 0, mParentWidget)); + mWaitDialog->setWindowFlags(mWaitDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint & ~Qt::WindowCloseButtonHint); + mWaitDialog->setWindowModality(Qt::WindowModal); + mWaitDialog->show(); +} + +void OMODFrameworkWrapper::hideWaitDialogSlot() { + if (mWaitDialog) { + mWaitDialog->hide(); + mWaitDialog.reset(); + } +} diff --git a/libs/installer_omod/src/OMODFrameworkWrapper.h b/libs/installer_omod/src/OMODFrameworkWrapper.h new file mode 100644 index 0000000..50251f0 --- /dev/null +++ b/libs/installer_omod/src/OMODFrameworkWrapper.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +#include + +#include "implementations/CodeProgress.h" +#include "QObject_unique_ptr.h" + +// define this here as it's going to be used a lot by things using this class' message box wrappers. +template +T& unused_out(T&& t) { return t; } + +class OMODFrameworkWrapper : public QObject +{ + Q_OBJECT + +public: + using EInstallResult = MOBase::IPluginInstaller::EInstallResult; + + OMODFrameworkWrapper(MOBase::IOrganizer* organizer, QWidget* parentWidget = nullptr); + + EInstallResult installInAnotherThread(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID); + + EInstallResult install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID); + + void setParentWidget(QWidget* parentWidget); + + void onInstallationEnd(EInstallResult status, MOBase::IModInterface* mod); + +protected: + void initFrameworkSettings(); + + void refreshFrameworkSettings(); + + void pushTempPath(const QString& tempPath); + + void popTempPath(); + +signals: + void pickModName(bool& successOut, MOBase::GuessedValue& modName); + + void createMod(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue& modName); + + void displayReadme(const QString& modName, const QString& readme); + + void showWaitDialog(QString message); + void hideWaitDialog(); + +protected slots: + void pickModNameSlot(bool& successOut, MOBase::GuessedValue& modName); + void createModSlot(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue& modName); + void displayReadmeSlot(const QString& modName, const QString& readme); + + void showWaitDialogSlot(QString message); + void hideWaitDialogSlot(); + +private: + __declspec(noinline) void constructorHelper(); + + MOBase::IOrganizer* mMoInfo; + QWidget* mParentWidget; + + QStack mTempPathStack; + + QObject_unique_ptr mWaitDialog; +}; diff --git a/libs/installer_omod/src/QObject_unique_ptr.h b/libs/installer_omod/src/QObject_unique_ptr.h new file mode 100644 index 0000000..e6f9395 --- /dev/null +++ b/libs/installer_omod/src/QObject_unique_ptr.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +static inline void deleteQObjectLater(QObject* qObject) { qObject->deleteLater(); } + +template +using QObject_unique_ptr = std::unique_ptr; + +template +QObject_unique_ptr make_unique(Args&&... args) +{ + return QObject_unique_ptr(new T(std::forward(args)...), &deleteQObjectLater); +} + +template +QObject_unique_ptr make_nullptr() +{ + return QObject_unique_ptr(nullptr, &deleteQObjectLater); +} diff --git a/libs/installer_omod/src/implementations/CodeProgress.cpp b/libs/installer_omod/src/implementations/CodeProgress.cpp new file mode 100644 index 0000000..7256592 --- /dev/null +++ b/libs/installer_omod/src/implementations/CodeProgress.cpp @@ -0,0 +1,85 @@ +#include + +#include "CodeProgress.h" + +CodeProgressHelper::CodeProgressHelper(QWidget* parentWidget) : mParentWidget{ parentWidget }, mProgressDialog(make_nullptr()) { + moveToThread(QApplication::instance()->thread()); + connect(this, &CodeProgressHelper::ShowProgressDialogSignal, this, &CodeProgressHelper::ShowProgressDialogSlot, Qt::QueuedConnection); + connect(this, &CodeProgressHelper::UpdateProgressValueSignal, this, &CodeProgressHelper::UpdateProgressValueSlot, Qt::BlockingQueuedConnection); + connect(this, &CodeProgressHelper::HideProgressDialogSignal, this, &CodeProgressHelper::HideProgressDialogSlot, Qt::QueuedConnection); + +} + +void CodeProgressHelper::ShowProgressDialog() { + emit ShowProgressDialogSignal(); +} + +void CodeProgressHelper::UpdateProgressValue(int percentage) { + emit UpdateProgressValueSignal(percentage); +} + +void CodeProgressHelper::HideProgressDialog() { + emit HideProgressDialogSignal(); +} + +void CodeProgressHelper::ShowProgressDialogSlot() { + mProgressDialog.reset(new QProgressDialog(mParentWidget)); + mProgressDialog->setWindowFlags(mProgressDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint & ~Qt::WindowCloseButtonHint); + mProgressDialog->setWindowModality(Qt::WindowModal); + mProgressDialog->setCancelButton(nullptr); + mProgressDialog->setMinimum(0); + mProgressDialog->setMaximum(100); + mProgressDialog->setAutoReset(false); + mProgressDialog->setAutoClose(false); + mProgressDialog->show(); +} + +void CodeProgressHelper::UpdateProgressValueSlot(int percentage) { + mProgressDialog->setValue(percentage); +} + +void CodeProgressHelper::HideProgressDialogSlot() { + mProgressDialog->hide(); + mProgressDialog.reset(); + } + +void CodeProgress::Init(__int64 totalSize, bool compressing) +{ + System::GC::ReRegisterForFinalize(this); + + mTotalSize = totalSize; + mCompressing = compressing; + mPercentage = 0; + + mHelper = new CodeProgressHelper(mParentWidget); + mHelper->ShowProgressDialog(); +} + +void CodeProgress::SetProgress(__int64 inSize, __int64 outSize) +{ + int newPercentage = (int)(100 * inSize / (double)mTotalSize); + if (newPercentage != mPercentage) + mHelper->UpdateProgressValue(newPercentage); + mPercentage = newPercentage; +} + +CodeProgress::~CodeProgress() +{ + if (!mHelper) + return; + + mHelper->HideProgressDialog(); + this->!CodeProgress(); +} + +CodeProgress::!CodeProgress() +{ + if (mHelper) + mHelper->deleteLater(); + mHelper = nullptr; +} + +void CodeProgress::setParentWidget(QWidget* parentWidget) +{ + mParentWidget = parentWidget; +} diff --git a/libs/installer_omod/src/implementations/CodeProgress.h b/libs/installer_omod/src/implementations/CodeProgress.h new file mode 100644 index 0000000..902fccd --- /dev/null +++ b/libs/installer_omod/src/implementations/CodeProgress.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +#include "../QObject_unique_ptr.h" + +using namespace cli; + +class CodeProgressHelper : public QObject { + Q_OBJECT +public: + + CodeProgressHelper(QWidget* parentWidget); + + void ShowProgressDialog(); + void UpdateProgressValue(int percentage); + void HideProgressDialog(); + +public slots: + void ShowProgressDialogSlot(); + void UpdateProgressValueSlot(int percentage); + void HideProgressDialogSlot(); + +signals: + + void ShowProgressDialogSignal(); + void UpdateProgressValueSignal(int percentage); + void HideProgressDialogSignal(); + +private: + QWidget* mParentWidget; + QObject_unique_ptr mProgressDialog; +}; + +ref class CodeProgress : OMODFramework::ICodeProgress +{ +public: + + CodeProgress(QWidget* parentWidget) : mParentWidget(parentWidget) { } + + virtual void Init(__int64 totalSize, bool compressing); + + virtual void SetProgress(__int64 inSize, __int64 outSize); + + ~CodeProgress(); + + !CodeProgress(); + + void setParentWidget(QWidget* parentWidget); + +private: + QWidget* mParentWidget; + CodeProgressHelper* mHelper; + __int64 mTotalSize; + int mPercentage; + bool mCompressing; +}; diff --git a/libs/installer_omod/src/implementations/Logger.cpp b/libs/installer_omod/src/implementations/Logger.cpp new file mode 100644 index 0000000..2b0a0c1 --- /dev/null +++ b/libs/installer_omod/src/implementations/Logger.cpp @@ -0,0 +1,45 @@ +#include "Logger.h" + +#include "../interop/StdDotNetConverters.h" + +OMODFramework::LoggingLevel Logger::OMODLoggingLevel(MOBase::log::Levels level) +{ + switch (level) + { + default: + case MOBase::log::Debug: + return OMODFramework::LoggingLevel::DEBUG; + case MOBase::log::Info: + return OMODFramework::LoggingLevel::INFO; + case MOBase::log::Warning: + return OMODFramework::LoggingLevel::WARNING; + case MOBase::log::Error: + return OMODFramework::LoggingLevel::ERROR; + } +} + +MOBase::log::Levels Logger::MOLoggingLevel(OMODFramework::LoggingLevel level) +{ + switch (level) + { + default: + case OMODFramework::LoggingLevel::DEBUG: + return MOBase::log::Debug; + case OMODFramework::LoggingLevel::INFO: + return MOBase::log::Info; + case OMODFramework::LoggingLevel::WARNING: + return MOBase::log::Warning; + case OMODFramework::LoggingLevel::ERROR: + return MOBase::log::Error; + } +} + +void Logger::Init() +{ + //no op +} + +void Logger::Log(OMODFramework::LoggingLevel level, System::String^ message, System::DateTime time) +{ + MOBase::log::getDefault().log(MOLoggingLevel(level), "{}", toUTF8String(message)); +} diff --git a/libs/installer_omod/src/implementations/Logger.h b/libs/installer_omod/src/implementations/Logger.h new file mode 100644 index 0000000..e3d1e7e --- /dev/null +++ b/libs/installer_omod/src/implementations/Logger.h @@ -0,0 +1,17 @@ +#pragma once + +using namespace cli; + +#include + +ref class Logger : OMODFramework::ILogger +{ +public: + static OMODFramework::LoggingLevel OMODLoggingLevel(MOBase::log::Levels level); + + static MOBase::log::Levels MOLoggingLevel(OMODFramework::LoggingLevel level); + + virtual void Init(); + + virtual void Log(OMODFramework::LoggingLevel level, System::String^ message, System::DateTime time); +}; diff --git a/libs/installer_omod/src/implementations/ScriptFunctions.cpp b/libs/installer_omod/src/implementations/ScriptFunctions.cpp new file mode 100644 index 0000000..be4deb3 --- /dev/null +++ b/libs/installer_omod/src/implementations/ScriptFunctions.cpp @@ -0,0 +1,320 @@ +#include "ScriptFunctions.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../interop/QtDotNetConverters.h" +#include "../newstuff/rtfPopup.h" +#include "../oldstuff/DialogSelect.h" + +ScriptFunctionsHelper::ScriptFunctionsHelper() : mMessageBoxHelper(make_unique()) +{ + moveToThread(QApplication::instance()->thread()); + + connect(this, &ScriptFunctionsHelper::DialogSelectSignal, this, &ScriptFunctionsHelper::DialogSelectSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::InputStringSignal, this, &ScriptFunctionsHelper::InputStringSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::DisplayImageSignal, this, &ScriptFunctionsHelper::DisplayImageSlot, Qt::BlockingQueuedConnection); + connect(this, &ScriptFunctionsHelper::DisplayTextSignal, this, &ScriptFunctionsHelper::DisplayTextSlot, Qt::BlockingQueuedConnection); +} + +std::optional> ScriptFunctionsHelper::DialogSelect(QWidget* parent, const QString& title, const QVector& items, const QVector& descriptions, const QVector& pixmaps, bool multiSelect) +{ + std::optional> result; + emit DialogSelectSignal(result, parent, title, items, descriptions, pixmaps, multiSelect); + return result; +} + +QString ScriptFunctionsHelper::InputString(QWidget* parentWidget, const QString& title, const QString& initialText) +{ + QString text; + emit InputStringSignal(text, parentWidget, title, initialText); + return text; +} + +void ScriptFunctionsHelper::DisplayImage(QWidget* parentWidget, const QString& path, const QString& title) +{ + emit DisplayImageSignal(parentWidget, path, title); +} + +void ScriptFunctionsHelper::DisplayText(QWidget* parentWidget, const QString& path, const QString& title) +{ + emit DisplayTextSignal(parentWidget, path, title); +} + +void ScriptFunctionsHelper::InputStringSlot(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText) +{ + textOut = QInputDialog::getText(parentWidget, title, title, QLineEdit::Normal, initialText); +} + +void ScriptFunctionsHelper::DisplayImageSlot(QWidget* parentWidget, const QString& path, const QString& title) +{ + QImageReader reader(path); + QImage image = reader.read(); + if (!image.isNull()) + { + QPixmap pixmap = QPixmap::fromImage(image); + MOBase::log::debug("image size {}, pixmap size {}", image.size(), pixmap.size()); + QDialog popup; + QLayout* layout = new QGridLayout(&popup); + popup.setLayout(layout); + FixedAspectRatioImageLabel* label = new FixedAspectRatioImageLabel(&popup); + label->setUnscaledPixmap(pixmap); + + QSize screenSize = parentWidget->screen()->availableSize(); + int maxHeight = static_cast(screenSize.height() * 0.8f); + if (pixmap.size().height() > maxHeight) + // This is approximate due to borders, label can sort out details. + popup.resize(label->widthForHeight(maxHeight), maxHeight); + + layout->addWidget(label); + popup.setWindowTitle(title); + popup.exec(); + } + else + MOBase::log::error("Unable to display {}. Error was {}: {}", path, reader.error(), reader.errorString()); +} + +void ScriptFunctionsHelper::DisplayTextSlot(QWidget* parentWidget, const QString& path, const QString& title) +{ + RtfPopup popup(toDotNetString(path), parentWidget); + popup.setWindowTitle(title); + // the size readmes are becoming automatically + popup.resize(492, 366); + popup.exec(); +} + +void ScriptFunctionsHelper::DialogSelectSlot(std::optional>& resultOut, QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, bool multiSelect) +{ + resultOut = ::DialogSelect(parent, title, items, descriptions, pixmaps, multiSelect); +} + +ScriptFunctions::ScriptFunctions(QWidget* parentWidget, MOBase::IOrganizer* moInfo) : mParentWidget(parentWidget), mMoInfo(moInfo), mHelper(new ScriptFunctionsHelper) {} + +ScriptFunctions::~ScriptFunctions() +{ + if (!mHelper) + return; + this->!ScriptFunctions(); +} + +ScriptFunctions::!ScriptFunctions() +{ + mHelper->deleteLater(); + mHelper = nullptr; +} + +void ScriptFunctions::Warn(System::String^ msg) +{ + mHelper->warning(mParentWidget, "Warning", toQString(msg)); +} + +void ScriptFunctions::Message(System::String^ msg) +{ + mHelper->information(mParentWidget, "Message", toQString(msg)); +} + +void ScriptFunctions::Message(System::String^ msg, System::String^ title) +{ + mHelper->information(mParentWidget, toQString(title), toQString(msg)); +} + +System::Collections::Generic::List^ ScriptFunctions::Select(System::Collections::Generic::List^ items, + System::String^ title, + bool isMultiSelect, + System::Collections::Generic::List^ previews, + System::Collections::Generic::List^ descriptions) +{ + QVector qItems; + qItems.reserve(items ? items->Count : 0); + QVector qPreviews; + qPreviews.reserve(previews ? previews->Count : 0); + QVector qDescriptions; + qDescriptions.reserve(descriptions ? descriptions->Count : 0); + + // Expect red squiggles. No one told intellisense about this syntax, but it's the least ugly. + if (items) + { + for each (System::String ^ item in items) + qItems.push_back(toQString(item)); + } + + if (previews) + { + for each (System::String ^ preview in previews) + qPreviews.push_back(toQString(preview)); + } + + if (descriptions) + { + for each (System::String ^ description in descriptions) + qDescriptions.push_back(toQString(description)); + } + + std::optional> qResponse = mHelper->DialogSelect(mParentWidget, toQString(title), qItems, qDescriptions, qPreviews, isMultiSelect); + if (!qResponse.has_value()) + return nullptr; + + System::Collections::Generic::List^ response = gcnew System::Collections::Generic::List(qResponse.value().length()); + for (const auto selection : qResponse.value()) + response->Add(selection); + return response; +} + +System::String^ ScriptFunctions::InputString(System::String^ title, System::String^ initialText) +{ + return toDotNetString(mHelper->InputString(mParentWidget, toQString(title), initialText ? toQString(initialText) : "")); +} + +int ScriptFunctions::DialogYesNo(System::String^ message) +{ + return mHelper->question(mParentWidget, "", toQString(message)) == QMessageBox::StandardButton::Yes; +} + +int ScriptFunctions::DialogYesNo(System::String^ message, System::String^ title) +{ + return mHelper->question(mParentWidget, toQString(title), toQString(message)) == QMessageBox::StandardButton::Yes; +} + +void ScriptFunctions::DisplayImage(System::String^ path, System::String^ title) +{ + mHelper->DisplayImage(mParentWidget, toQString(path), toQString(title)); +} + +void ScriptFunctions::DisplayText(System::String^ text, System::String^ title) +{ + mHelper->DisplayText(mParentWidget, toQString(text), toQString(title)); +} + +void ScriptFunctions::Patch(System::String^ from, System::String^ to) +{ + throw gcnew System::NotImplementedException(); +} + +System::String^ ScriptFunctions::ReadOblivionINI(System::String^ section, System::String^ name) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +System::String^ ScriptFunctions::ReadRendererInfo(System::String^ name) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +bool ScriptFunctions::DataFileExists(System::String^ path) +{ + return mMoInfo->resolvePath(toQString(path)) != ""; +} + +bool ScriptFunctions::HasScriptExtender() +{ + for (const auto& forcedLoad : mMoInfo->managedGame()->executableForcedLoads()) + { + if (forcedLoad.library().toLower().startsWith("obse")) + { + if (mMoInfo->managedGame()->gameDirectory().exists(forcedLoad.library())) + return true; + } + } + + return mMoInfo->managedGame()->gameDirectory().exists("obse_loader.exe"); +} + +bool ScriptFunctions::HasGraphicsExtender() +{ + return DataFileExists("obse\\plugins\\obge.dll"); +} + +System::Version^ ScriptFunctions::ScriptExtenderVersion() +{ + QString obsePath; + for (const auto& forcedLoad : mMoInfo->managedGame()->executableForcedLoads()) + { + if (forcedLoad.library().toLower().startsWith("obse")) + { + if (mMoInfo->managedGame()->gameDirectory().exists(forcedLoad.library())) + { + obsePath = mMoInfo->managedGame()->gameDirectory().filePath(forcedLoad.library()); + break; + } + } + } + + if (obsePath.isEmpty()) + obsePath = mMoInfo->managedGame()->gameDirectory().filePath("obse_loader.exe"); + + System::Diagnostics::FileVersionInfo^ info = System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(obsePath)); + return gcnew System::Version(info->FileMajorPart, info->FileMinorPart, info->FileBuildPart, info->FilePrivatePart); +} + +System::Version^ ScriptFunctions::GraphicsExtenderVersion() +{ + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(mMoInfo->resolvePath("obse\\plugins\\obge.dll")))->FileVersion); +} + +System::Version^ ScriptFunctions::OblivionVersion() +{ + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(mMoInfo->managedGame()->gameDirectory().filePath("oblivion.exe")))->FileVersion); +} + +System::Version^ ScriptFunctions::OBSEPluginVersion(System::String^ path) +{ + QString pluginPath = mMoInfo->resolvePath(toQString(System::IO::Path::Combine("obse", "plugins", System::IO::Path::ChangeExtension(path, ".dll")))); + if (pluginPath.isEmpty()) + return nullptr; + return gcnew System::Version(System::Diagnostics::FileVersionInfo::GetVersionInfo(toDotNetString(pluginPath))->FileVersion); +} + +System::Collections::Generic::IEnumerable^ ScriptFunctions::GetESPs() +{ + QStringList plugins = mMoInfo->pluginList()->pluginNames(); + System::Collections::Generic::List^ pluginList = gcnew System::Collections::Generic::List(plugins.count()); + for (const auto& pluginName : plugins) + { + auto state = mMoInfo->pluginList()->state(pluginName); + if (state != MOBase::IPluginList::PluginState::STATE_MISSING) + { + OMODFramework::Scripting::ScriptESP plugin; + plugin.Name = toDotNetString(pluginName); + plugin.Active = state == MOBase::IPluginList::PluginState::STATE_ACTIVE; + pluginList->Add(plugin); + } + } + return pluginList; +} + +System::Collections::Generic::IEnumerable^ ScriptFunctions::GetActiveOMODNames() +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. No known OMODs seem to actually use this (which is irritating as this is one of OBMM's most powerful features). +} + +cli::array^ ScriptFunctions::ReadExistingDataFile(System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +cli::array^ ScriptFunctions::GetDataFileFromBSA(System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} + +cli::array^ ScriptFunctions::GetDataFileFromBSA(System::String^ bsa, System::String^ file) +{ + throw gcnew System::NotImplementedException(); + // TODO: implement this if a user ever reports the exception. OMODFramework should be handling this for us. +} diff --git a/libs/installer_omod/src/implementations/ScriptFunctions.h b/libs/installer_omod/src/implementations/ScriptFunctions.h new file mode 100644 index 0000000..28f9dee --- /dev/null +++ b/libs/installer_omod/src/implementations/ScriptFunctions.h @@ -0,0 +1,122 @@ +#pragma once + +using namespace cli; + +#include + +#include + +#include + +#include "../MessageBoxHelper.h" +#include "../QObject_unique_ptr.h" + +class ScriptFunctionsHelper : public QObject +{ + Q_OBJECT + +public: + ScriptFunctionsHelper(); + + // don't bother with std::forward and decltype(auto) as we know everything is fine being copied + // in fact, as some stuff lives on the managed heap, we can't take a reference anyway + template auto critical(Args... args) { return mMessageBoxHelper->critical(args...); } + template auto information(Args... args) { return mMessageBoxHelper->information(args...); } + template auto question(Args... args) { return mMessageBoxHelper->question(args...); } + template auto warning(Args... args) { return mMessageBoxHelper->warning(args...); } + + std::optional> DialogSelect(QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, + bool multiSelect); + + QString InputString(QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImage(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayText(QWidget* parentWidget, const QString& path, const QString& title); + +signals: + void DialogSelectSignal(std::optional>& resultOut, QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, bool multiSelect); + + void InputStringSignal(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImageSignal(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayTextSignal(QWidget* parentWidget, const QString& path, const QString& title); + +public slots: + void DialogSelectSlot(std::optional>& resultOut, QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, bool multiSelect); + + void InputStringSlot(QString& textOut, QWidget* parentWidget, const QString& title, const QString& initialText); + + void DisplayImageSlot(QWidget* parentWidget, const QString& path, const QString& title); + + void DisplayTextSlot(QWidget* parentWidget, const QString& path, const QString& title); + +private: + QObject_unique_ptr mMessageBoxHelper; +}; + +ref class ScriptFunctions : OMODFramework::Scripting::IScriptFunctions +{ +public: + ScriptFunctions(QWidget* parentWidget, MOBase::IOrganizer* moInfo); + ~ScriptFunctions(); + !ScriptFunctions(); + + // note: C++/CLI wants virtual for interface implementations, not override + virtual void Warn(System::String^ msg); + + virtual void Message(System::String^ msg); + + virtual void Message(System::String^ msg, System::String^ title); + + virtual System::Collections::Generic::List^ Select(System::Collections::Generic::List^ items, System::String^ title, bool isMultiSelect, System::Collections::Generic::List^ previews, System::Collections::Generic::List^ descriptions); + + virtual System::String^ InputString(System::String^ title, System::String^ initialText); + + virtual int DialogYesNo(System::String^ message); + + virtual int DialogYesNo(System::String^ message, System::String^ title); + + virtual void DisplayImage(System::String^ path, System::String^ title); + + virtual void DisplayText(System::String^ text, System::String^ title); + + virtual void Patch(System::String^ from, System::String^ to); + + virtual System::String^ ReadOblivionINI(System::String^ section, System::String^ name); + + virtual System::String^ ReadRendererInfo(System::String^ name); + + virtual bool DataFileExists(System::String^ path); + + virtual bool HasScriptExtender(); + + virtual bool HasGraphicsExtender(); + + virtual System::Version^ ScriptExtenderVersion(); + + virtual System::Version^ GraphicsExtenderVersion(); + + virtual System::Version^ OblivionVersion(); + + virtual System::Version^ OBSEPluginVersion(System::String^ path); + + virtual System::Collections::Generic::IEnumerable^ GetESPs(); + + virtual System::Collections::Generic::IEnumerable^ GetActiveOMODNames(); + + virtual cli::array^ ReadExistingDataFile(System::String^ file); + + virtual cli::array^ GetDataFileFromBSA(System::String^ file); + + virtual cli::array^ GetDataFileFromBSA(System::String^ bsa, System::String^ file); + +private: + QWidget* mParentWidget; + MOBase::IOrganizer* mMoInfo; + ScriptFunctionsHelper* mHelper; +}; diff --git a/libs/installer_omod/src/installerOmod.cpp b/libs/installer_omod/src/installerOmod.cpp new file mode 100644 index 0000000..3e4c390 --- /dev/null +++ b/libs/installer_omod/src/installerOmod.cpp @@ -0,0 +1,204 @@ +#include "installerOmod.h" + +#include +#include + +#include +#include + +#include "OMODFrameworkWrapper.h" + +InstallerOMOD::InstallerOMOD() : mMoInfo(nullptr), mOmodFrameworkWrapper(nullptr) +{ +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPlugin + +bool InstallerOMOD::init(MOBase::IOrganizer* moInfo) +{ + mMoInfo = moInfo; + mOmodFrameworkWrapper = std::make_unique(mMoInfo); + mMoInfo->onAboutToRun([this](const QString&) { buildSDPs(); return true; }); + mMoInfo->onFinishedRun([this](const QString&, unsigned int) { clearSDPs(); }); + return true; +} + +QString InstallerOMOD::name() const +{ + return "Omod Installer"; +} + +QString InstallerOMOD::localizedName() const +{ + return tr("Omod Installer"); +} + +QString InstallerOMOD::author() const +{ + return "AnyOldName3 & erril120"; +} + +QString InstallerOMOD::description() const +{ + return tr("Installer for Omod files (including scripted ones)"); +} + +MOBase::VersionInfo InstallerOMOD::version() const +{ + return MOBase::VersionInfo(1, 1, 0, MOBase::VersionInfo::RELEASE_FINAL); +} + +std::vector> InstallerOMOD::requirements() const +{ + return { Requirements::gameDependency("Oblivion") }; +} + +QList InstallerOMOD::settings() const +{ + return {}; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPluginInstaller + +unsigned int InstallerOMOD::priority() const +{ + // Some other installers have a use_any_file setting and then they'll try and claim OMODs as their own, so we want higher priority than them. + return 500; +} + +bool InstallerOMOD::isManualInstaller() const +{ + return false; +} + +void InstallerOMOD::onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) +{ + mOmodFrameworkWrapper->onInstallationEnd(result, newMod); +} + +bool InstallerOMOD::isArchiveSupported(std::shared_ptr tree) const +{ + for (const auto file : *tree) { + // config is the only file guaranteed to be there + if (file->isFile() && file->name() == "config") + return true; + } + return false; +} + +void InstallerOMOD::setParentWidget(QWidget* parent) +{ + IPluginInstaller::setParentWidget(parent); + mOmodFrameworkWrapper->setParentWidget(parent); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IPluginInstallerCustom + +bool InstallerOMOD::isArchiveSupported(const QString& archiveName) const +{ + return archiveName.endsWith(".omod", Qt::CaseInsensitive); +} + +std::set InstallerOMOD::supportedExtensions() const +{ + return { "omod" }; +} + +InstallerOMOD::EInstallResult InstallerOMOD::install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) +{ + return mOmodFrameworkWrapper->installInAnotherThread(modName, gameName, archiveName, version, nexusID); +} + +MappingType InstallerOMOD::mappings() const +{ + MOBase::log::debug("Mapping virtual SDPs"); + + MappingType mappings; + + for (const auto& [realSDPPath, virtualSDP] : mVirtualSDPs) + mappings.push_back({ virtualSDP->fileName(), realSDPPath, false, false }); + + return mappings; +} + +void InstallerOMOD::buildSDPs() +{ + QStringList directories = mMoInfo->listDirectories("Shaders/OMOD"); + directories = directories.filter(QRegularExpression("\\d+")); + + for (const auto& directory : directories) + { + QString packagePath = mMoInfo->resolvePath(QString("Shaders/shaderpackage%1.sdp").arg(directory.toInt(), 3, 10, QChar('0'))); + if (packagePath.isEmpty()) + continue; + + QMap shaders; + { + QFile packageFile(packagePath); + packageFile.open(QIODevice::ReadOnly); + QDataStream basePackageStream(&packageFile); + basePackageStream.setByteOrder(QDataStream::LittleEndian); + quint32 magic; + basePackageStream >> magic; + if (magic != 100) + { + MOBase::log::debug("SDP magic number mismatch in {}, got {}, but expected 100", packagePath, magic); + continue; + } + quint32 shaderCount, totalSize; + basePackageStream >> shaderCount >> totalSize; + for (quint32 i = 0; i < shaderCount && !basePackageStream.atEnd(); ++i) + { + QByteArray shaderName(256, '\0'); + basePackageStream.readRawData(shaderName.data(), 256); + quint32 shaderSize; + basePackageStream >> shaderSize; + QByteArray shaderBytecode(shaderSize, '\0'); + basePackageStream.readRawData(shaderBytecode.data(), shaderSize); + shaders[shaderName] = shaderBytecode; + } + } + + QStringList shaderFiles = mMoInfo->findFiles("Shaders/OMOD/" + directory, { "*.vso", "*.pso" }); + for (const auto& shaderFile : shaderFiles) + { + QFileInfo info(shaderFile); + QByteArray shaderName = (info.baseName().toUpper() + "." + info.suffix().toLower()).toLatin1(); + QFile file(shaderFile); + file.open(QIODevice::ReadOnly); + shaderName = shaderName.leftJustified(256, '\0', true); + shaders[shaderName] = file.readAll(); + MOBase::log::debug("Replacement for {} was {} bytes long", shaderFile, shaders[shaderName].size()); + } + + std::unique_ptr outputFile = std::make_unique(); + outputFile->open(); + QDataStream packageStream(outputFile.get()); + packageStream.setByteOrder(QDataStream::LittleEndian); + packageStream << quint32(100) << quint32(shaders.count()); + + quint32 totalSize = 0; + for (const auto& shader : shaders) + totalSize += 256 + 4 + shader.size(); + packageStream << totalSize; + + for (auto itr = shaders.cbegin(); itr != shaders.cend(); ++itr) + { + packageStream.writeRawData(itr.key().constData(), 256); + packageStream << quint32(itr.value().size()); + packageStream.writeRawData(itr.value().constData(), itr.value().size()); + } + + outputFile->close(); + + mVirtualSDPs.emplace(mMoInfo->managedGame()->dataDirectory().absoluteFilePath(QString("Shaders/shaderpackage%1.sdp").arg(directory.toInt(), 3, 10, QChar('0'))), std::move(outputFile)); + } +} + +void InstallerOMOD::clearSDPs() +{ + mVirtualSDPs.clear(); +} diff --git a/libs/installer_omod/src/installerOmod.h b/libs/installer_omod/src/installerOmod.h new file mode 100644 index 0000000..a122355 --- /dev/null +++ b/libs/installer_omod/src/installerOmod.h @@ -0,0 +1,75 @@ +#ifndef INSTALLEROMOD_H +#define INSTALLEROMOD_H + +#include + +#include +#include + +#include "OMODFrameworkWrapper.h" + +class InstallerOMOD : public MOBase::IPluginInstallerCustom, public MOBase::IPluginFileMapper +{ + Q_OBJECT; + Q_INTERFACES(MOBase::IPlugin MOBase::IPluginInstaller MOBase::IPluginInstallerCustom MOBase::IPluginFileMapper); +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + // We should probably buy a domain. + Q_PLUGIN_METADATA(IID "org.AnyOldName3.InstallerOmod"); +#endif + +public: + InstallerOMOD(); + + // IPlugin + bool init(MOBase::IOrganizer* moInfo) override; + + QString name() const override; + + QString localizedName() const override; + + QString author() const override; + + QString description() const override; + + MOBase::VersionInfo version() const override; + + std::vector> requirements() const override; + + QList settings() const override; + + // IPluginInstaller + + unsigned int priority() const override; + + bool isManualInstaller() const override; + + void onInstallationEnd(EInstallResult result, MOBase::IModInterface* newMod) override; + + bool isArchiveSupported(std::shared_ptr tree) const override; + + void setParentWidget(QWidget* parent) override; + + // IPluginInstallerCustom + + bool isArchiveSupported(const QString& archiveName) const override; + + std::set supportedExtensions() const override; + + EInstallResult install(MOBase::GuessedValue& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID) override; + + // IPluginFileMapper + + MappingType mappings() const override; + +protected: + void buildSDPs(); + + void clearSDPs(); + +private: + MOBase::IOrganizer* mMoInfo; + std::unique_ptr mOmodFrameworkWrapper; + std::map> mVirtualSDPs; +}; + +#endif // !INSTALLEROMOD_H diff --git a/libs/installer_omod/src/installer_omod_en.ts b/libs/installer_omod/src/installer_omod_en.ts new file mode 100644 index 0000000..a092611 --- /dev/null +++ b/libs/installer_omod/src/installer_omod_en.ts @@ -0,0 +1,159 @@ + + + + + InstallerOMOD + + + Omod Installer + + + + + Installer for Omod files (including scripted ones) + + + + + NameDialog + + + Pick mod name + + + + + Name + + + + + + Manual install is unavailable for OMODs. + + + + + Manual + + + + + OK + + + + + Cancel + + + + + OMODFrameworkWrapper + + + %1 wants to change [%2] %3 from "%4" to "%5" + %1 is the mod name [%2] is the ini section name. %3 is the ini setting name. %4 is the value already in Oblivion.ini. %5 is the value the mod wants to set. + + + + + %1 wants to set [%2] %3 to "%4" + %1 is the mod name [%2] is the ini section name. %3 is the ini setting name. %5 is the value the mod wants to set. + + + + + Update INI? + + + + + %1 has data for %2, but Mod Organizer 2 doesn't know what to do with it yet. Please report this to the Mod Organizer 2 development team (ideally by sending us your interface log) as we didn't find any OMODs that actually did this, and we need to know that they exist. + %1 is the mod name %2 is the name of a field in the OMOD's return data Hopefully this message will never be seen by anyone, but if it is, they need to know to tell the Mod Organizer 2 dev team. + + + + + Mod Organizer 2 can't completely install this OMOD. + + + + + Activate mod? + + + + + %1 contains the OMOD %2. OMODs may have post-installation actions like activating ESPs. Would you like to enable the mod so this can happen now? + %1 is the left-pane mod name. %2 is the name from the metadata of an OMOD. + + + + + %1 wants to activate %2. Do you want to do so? + %1 is the mod name. %2 is the plugin name. + + + + + + Activate plugin? + + + + + OMOD wants to activate missing plugin + + + + + An OMOD wants to activate a missing plugin. This shouldn't be possible. Please report this to a MO2 developer. + + + + + %1 installed %2, but doesn't activate it by default. Do you want to activate it anyway? + %1 is the mod name. %2 is the plugin name. + + + + + OMOD claimed to install missing plugin + + + + + An OMOD has activation settings for a missing plugin. This shouldn't be possible. Please report this to a MO2 developer. + + + + + Register BSAs + + + + + %1 wants to register the following BSA archives, but Mod Organizer 2 can't do that yet due to technical limitations:<ul><li>%2</li></ul>For now, your options include adding the BSA names to <code>sResourceArchiveList</code> in the game INI, creating a dummy ESP with the same name, or extracting the BSA, all of which have drawbacks. + %1 is the OMOD name <ul><li>%2</li></ul> becomes a list of BSA files + + + + + Display Readme? + + + + + The Readme may explain installation options. Display it?<br>It will remain visible until you close it. + <br> is a line break. Translators can remove it if it makes things clearer. + + + + + %1 Readme + %1 is the mod name + + + + diff --git a/libs/installer_omod/src/interop/QtDotNetConverters.cpp b/libs/installer_omod/src/interop/QtDotNetConverters.cpp new file mode 100644 index 0000000..430e8e5 --- /dev/null +++ b/libs/installer_omod/src/interop/QtDotNetConverters.cpp @@ -0,0 +1,17 @@ +#include "QtDotNetConverters.h" + +#include + +#include + +QString toQString(System::String^ string) +{ + if (string) + return QString::fromStdWString(msclr::interop::marshal_as(string)); + return QString(); +} + +System::String^ toDotNetString(const QString& qString) +{ + return qString.isNull() ? nullptr : msclr::interop::marshal_as(qString.toStdWString()); +} diff --git a/libs/installer_omod/src/interop/QtDotNetConverters.h b/libs/installer_omod/src/interop/QtDotNetConverters.h new file mode 100644 index 0000000..6cca053 --- /dev/null +++ b/libs/installer_omod/src/interop/QtDotNetConverters.h @@ -0,0 +1,9 @@ +#pragma once + +using namespace cli; + +#include + +QString toQString(System::String^ string); + +System::String^ toDotNetString(const QString& qString); diff --git a/libs/installer_omod/src/interop/StdDotNetConverters.cpp b/libs/installer_omod/src/interop/StdDotNetConverters.cpp new file mode 100644 index 0000000..be7497b --- /dev/null +++ b/libs/installer_omod/src/interop/StdDotNetConverters.cpp @@ -0,0 +1,35 @@ +#include "stdDotNetConverters.h" + +#include + +#include + +std::wstring toWString(System::String^ string) +{ + if (string) + return msclr::interop::marshal_as(string); + return L"null .NET string"; +} + +System::String^ toDotNetString(const std::wstring& wString) +{ + return msclr::interop::marshal_as(wString); +} + +std::string toUTF8String(System::String^ string) +{ + if (string) + { + array^ utf8 = System::Text::Encoding::UTF8->GetBytes(string); + std::string stdstring; + stdstring.resize(utf8->Length); + System::Runtime::InteropServices::Marshal::Copy(utf8, 0, System::IntPtr(stdstring.data()), utf8->Length); + return stdstring; + } + return "null .NET string"; +} + +std::exception toStdException(System::Exception^ exception) +{ + return std::runtime_error("Unhanded .NET Exception: " + toUTF8String(exception->ToString())); +} diff --git a/libs/installer_omod/src/interop/StdDotNetConverters.h b/libs/installer_omod/src/interop/StdDotNetConverters.h new file mode 100644 index 0000000..04c2ada --- /dev/null +++ b/libs/installer_omod/src/interop/StdDotNetConverters.h @@ -0,0 +1,13 @@ +#pragma once + +using namespace cli; + +#include + +std::wstring toWString(System::String^ string); + +std::string toUTF8String(System::String^ string); + +System::String^ toDotNetString(const std::wstring& wString); + +std::exception toStdException(System::Exception^ exception); diff --git a/libs/installer_omod/src/newstuff/namedialog.cpp b/libs/installer_omod/src/newstuff/namedialog.cpp new file mode 100644 index 0000000..4dd44d6 --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.cpp @@ -0,0 +1,37 @@ +#include "namedialog.h" + +#include + +NameDialog::NameDialog(const MOBase::GuessedValue& suggestedNames, QWidget* parent) + : QDialog(parent), ui(), mName(suggestedNames) +{ + ui.setupUi(this); + + for (const auto& name : suggestedNames.variants()) + ui.nameCombo->addItem(name); + + ui.nameCombo->setCurrentIndex(ui.nameCombo->findText(suggestedNames)); + + setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); + ui.nameCombo->completer()->setCaseSensitivity(Qt::CaseSensitive); +} + +QString NameDialog::getName() const +{ + return mName; +} + +void NameDialog::on_okBtn_clicked() +{ + accept(); +} + +void NameDialog::on_cancelBtn_clicked() +{ + reject(); +} + +void NameDialog::on_nameCombo_currentTextChanged(const QString& text) +{ + mName = text; +} diff --git a/libs/installer_omod/src/newstuff/namedialog.h b/libs/installer_omod/src/newstuff/namedialog.h new file mode 100644 index 0000000..72e62eb --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +#include "ui_namedialog.h" + +class NameDialog : public QDialog +{ + Q_OBJECT + +public: + NameDialog(const MOBase::GuessedValue& suggestedNames, QWidget* parent = nullptr); + + QString getName() const; + +private slots: + void on_okBtn_clicked(); + + void on_cancelBtn_clicked(); + + void on_nameCombo_currentTextChanged(const QString& text); + +private: + Ui::NameDialog ui; + QString mName; +}; diff --git a/libs/installer_omod/src/newstuff/namedialog.ui b/libs/installer_omod/src/newstuff/namedialog.ui new file mode 100644 index 0000000..5727d1a --- /dev/null +++ b/libs/installer_omod/src/newstuff/namedialog.ui @@ -0,0 +1,89 @@ + + + NameDialog + + + + 0 + 0 + 400 + 83 + + + + Pick mod name + + + + + + + + Name + + + + + + + true + + + + + + + + + + + false + + + Manual install is unavailable for OMODs. + + + Manual install is unavailable for OMODs. + + + Manual + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + true + + + + + + + Cancel + + + + + + + + + + diff --git a/libs/installer_omod/src/newstuff/rtfPopup.cpp b/libs/installer_omod/src/newstuff/rtfPopup.cpp new file mode 100644 index 0000000..476f078 --- /dev/null +++ b/libs/installer_omod/src/newstuff/rtfPopup.cpp @@ -0,0 +1,33 @@ +#include "rtfPopup.h" + +#include +#include +#include +#include +#include + +#include "../interop/QtDotNetConverters.h" + +RtfPopup::RtfPopup(System::String^ rtfText, QWidget* parent, Qt::WindowFlags f) : QDialog(parent, f) +{ + QString text = rtfText->StartsWith("{\\rtf") ? toQString(RtfPipe::Rtf::ToHtml(rtfText)) : Qt::convertFromPlainText(toQString(rtfText), Qt::WhiteSpaceNormal); + QRegularExpression urlFinder(R"REGEX((?\1)"); + + QLayout* layout = new QGridLayout(this); + setLayout(layout); + QScrollArea* scrollArea = new QScrollArea(this); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + layout->addWidget(scrollArea); + + QLabel* label = new QLabel(text, scrollArea); + label->setWordWrap(true); + label->setTextFormat(Qt::RichText); + label->setOpenExternalLinks(true); + label->setTextInteractionFlags(Qt::TextBrowserInteraction); + scrollArea->setWidget(label); + scrollArea->setWidgetResizable(true); + + setSizeGripEnabled(true); +} diff --git a/libs/installer_omod/src/newstuff/rtfPopup.h b/libs/installer_omod/src/newstuff/rtfPopup.h new file mode 100644 index 0000000..17ca10c --- /dev/null +++ b/libs/installer_omod/src/newstuff/rtfPopup.h @@ -0,0 +1,11 @@ +using namespace cli; + +#include + +class RtfPopup : public QDialog +{ + Q_OBJECT + +public: + RtfPopup(System::String^ rtfText, QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); +}; diff --git a/libs/installer_omod/src/oldstuff/DialogSelect.cpp b/libs/installer_omod/src/oldstuff/DialogSelect.cpp new file mode 100644 index 0000000..9d387a8 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/DialogSelect.cpp @@ -0,0 +1,301 @@ +#include "DialogSelect.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MIT-licencedCodeToDoStuff/checkboxwordwrap.h" + +// there is no hover signal, the only way to know is to override enterEvent() +// +template +class HoverableWidget : public T +{ +public: + std::function onHover; + + HoverableWidget(Args... args, std::function h) + : T(args...), onHover(std::move(h)) + { + } + +protected: void enterEvent(QEnterEvent*) override + { + onHover(); + } +}; + + +FixedAspectRatioImageLabel::FixedAspectRatioImageLabel(QWidget* parent) : QLabel(parent) +{ +} + +void FixedAspectRatioImageLabel::setUnscaledPixmap(const QPixmap& pixmap) +{ + mUnscaledPixmap = pixmap; + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + rescalePixmap(size()); + + copySizeLabel.setPixmap(pixmap); +} + +const QPixmap& FixedAspectRatioImageLabel::unscaledPixmap() const +{ + return mUnscaledPixmap; +} + +QSize FixedAspectRatioImageLabel::sizeHint() const +{ + if (mUnscaledPixmap.isNull()) + { + return QLabel::sizeHint(); + } + else + { + // maybe should add frame border + return mUnscaledPixmap.size(); + //return copySizeLabel.sizeHint(); + } +} + +bool FixedAspectRatioImageLabel::hasHeightForWidth() const +{ + return true; +} + +int FixedAspectRatioImageLabel::heightForWidth(int width) const +{ + // this ignores the difference between size and contentsRect size + return mUnscaledPixmap.height() * width / (double)mUnscaledPixmap.width(); +} + +int FixedAspectRatioImageLabel::widthForHeight(int height) const +{ + return mUnscaledPixmap.width() * height / (double)mUnscaledPixmap.height(); +} + +void FixedAspectRatioImageLabel::resizeEvent(QResizeEvent* resizeEvent) +{ + QLabel::resizeEvent(resizeEvent); + rescalePixmap(contentsRect().size()); +} + +void FixedAspectRatioImageLabel::rescalePixmap(const QSize& size) +{ + setPixmap(mUnscaledPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + + +std::optional> DialogSelect( + QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, + bool multiSelect) +{ + QDialog d(parent); + d.setWindowTitle(title); + + auto* mainLayout = new QVBoxLayout(&d); + d.setLayout(mainLayout); + + auto* splitter = new QSplitter(&d); + mainLayout->addWidget(splitter); + + // button box + auto* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok|QDialogButtonBox::Cancel, &d); + + /* QObject::connect: signal not found in QDialogButtonBox + even though this worked with my initial attempt. + Holt had the same issue: https://stackoverflow.com/questions/61879664/qobjectconnect-not-working-signal-not-found-with-function-syntax + The new syntax is much better, but as this code wants replacing, that's not a hill I'm going to die on. + QObject::connect(buttons, &QDialogButtonBox::accepted, [&] { + d.accept(); + }); + + QObject::connect(buttons, &QDialogButtonBox::rejected, [&] { + d.reject(); + }); + */ + QObject::connect(buttons, SIGNAL(accepted()), &d, SLOT(accept())); + QObject::connect(buttons, SIGNAL(rejected()), &d, SLOT(reject())); + + mainLayout->addWidget(buttons); + + + // left panel + auto* left = new QWidget(splitter); + auto* leftLayout = new QVBoxLayout(left); + leftLayout->setContentsMargins(0, 0, 0, 0); + + auto* stack = new QStackedWidget(left); + leftLayout->addWidget(stack); + + // don't put descriptions of pixmaps at all if there aren't any + const bool hasDescriptions = !descriptions.empty(); + const bool hasPixmaps = !pixmaps.empty(); + + // for each description/item + for (int i=0; i < std::max(descriptions.size(), pixmaps.size()); ++i) { + auto* panel = new QWidget(left); + auto* panelLayout = new QVBoxLayout(panel); + //panelLayout->setContentsMargins(0, 0, 0, 0); + + if (hasPixmaps) { + auto* pixmapLabel = new FixedAspectRatioImageLabel(panel); + + pixmapLabel->setFrameStyle(QFrame::StyledPanel); + pixmapLabel->setLineWidth(1); + + // make it resizable + //pixmapLabel->setMinimumSize(1, 150); + //pixmapLabel->setScaledContents(true); + //pixmapLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + + if (i < pixmaps.size()) { + // this item has a pixmap + // Use QImageReader as (despite what the documentation says) QPixmap uses the extension, rather than the file header, to determine the format. + QImageReader reader(pixmaps[i]); + reader.setDecideFormatFromContent(true); + QImage image = reader.read(); + if (image.isNull()) { + pixmapLabel->setText(QString("failed to load '%1': %2 (code %3)").arg(pixmaps[i], reader.errorString(), QString::number(reader.error()))); + } else { + QPixmap pixmap = QPixmap::fromImage(image); + pixmapLabel->setUnscaledPixmap(std::move(pixmap)); + } + } + + panelLayout->addWidget(pixmapLabel); + } + + if (hasDescriptions) + { + // description under pixmap (if any) + auto* description = new QPlainTextEdit(descriptions[i], panel); + + // this puts the description on top if there's no pixmap + //description->setAlignment(Qt::AlignLeft|Qt::AlignTop); + + //description->setWordWrap(true); + + panelLayout->addWidget(description); + } + + // if the pixmap is first, it'll take as much space as it can; if the + // description is first, the AlignTop makes it work anyways + panelLayout->setStretch(0, 1); + + stack->addWidget(panel); + } + + + // right panel + auto* right = new QWidget(&d); + auto* rightOuterLayout = new QVBoxLayout(right); + rightOuterLayout->setContentsMargins(0, 0, 0, 0); + + // title + auto* titleLabel = new QLabel(title, right); + titleLabel->setWordWrap(true); + rightOuterLayout->addWidget(titleLabel); + + QWidget* rightInner; + QBoxLayout* rightInnerLayout; + + // Doing this unconditionally breaks HGEC lower body choices. + // Somehow the fixed aspect ratio image label forces the scroll area to be resized below its preferred size. + // Because the word wrapped radio buttons aren't good at telling Qt their size hint is only minimal for the current width, + // the layout ends up with their preferred width as its minimum and won't let itself be shrunk smaller than that even though you can't see the right hand side. + // This means that as they're in a widget with enough space, the text isn't wrapped. + // The exact same behaviour happens with non-wrappable radio buttons, but it's more expected. + // Making the fixed aspect ratio image label play nicely with being shrunk will probably fix this. + // It only seems to be MEAT that actually needs a scrollbar, though, and that looks fine, so fixing this mess is left as an exercise for the reader. + if (items.size() >= 10) + { + QScrollArea* rightScrollArea = new QScrollArea(right); + rightOuterLayout->addWidget(rightScrollArea); + rightScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + rightScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + rightInner = new QWidget(right); + rightScrollArea->setWidget(rightInner); + rightScrollArea->setWidgetResizable(true); + + rightInnerLayout = new QVBoxLayout(rightInner); + } + else + { + rightInner = right; + rightInnerLayout = rightOuterLayout; + } + + // callback when hovering + auto onHover = [&](int i) { + stack->setCurrentIndex(i); + }; + + + // remember buttons to see which were checked + QVector itemButtons; + + // for each item + for (int i=0; i(labelText, rightInner, [=]{ onHover(i); }); + } else { + w = new HoverableWidget(labelText, rightInner, [=]{ onHover(i); }); + checked |= i == 0; + } + + rightInnerLayout->addWidget(w); + itemButtons.push_back(w); + w->setChecked(checked); + } + + // push all the items to the top + rightInnerLayout->addStretch(1); + + splitter->addWidget(left); + splitter->addWidget(right); + + // decent initial size + d.resize(800, 500); + + if (d.exec() != QDialog::Accepted) { + return std::nullopt; + } + + + // go through every button, it doesn't matter whether they're checkboxes or + // radio buttons + QVector selection; + for (int i=0; iisChecked()) { + selection.push_back(i); + } + } + + return selection; +} diff --git a/libs/installer_omod/src/oldstuff/DialogSelect.h b/libs/installer_omod/src/oldstuff/DialogSelect.h new file mode 100644 index 0000000..0e0f1bb --- /dev/null +++ b/libs/installer_omod/src/oldstuff/DialogSelect.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include + +std::optional> DialogSelect(QWidget* parent, const QString& title, const QVector& items, + const QVector& descriptions, const QVector& pixmaps, + bool multiSelect); + + +// For some reason, this can be resized bigger but not smaller. +class FixedAspectRatioImageLabel : public QLabel +{ + Q_OBJECT + + Q_PROPERTY(QPixmap unscaledPixmap READ unscaledPixmap WRITE setUnscaledPixmap) + +public: + FixedAspectRatioImageLabel() = default; + + FixedAspectRatioImageLabel(QWidget* parent); + + void setUnscaledPixmap(const QPixmap& pixmap); + + const QPixmap& unscaledPixmap() const; + + QSize sizeHint() const override; + + bool hasHeightForWidth() const override; + + int heightForWidth(int width) const override; + + int widthForHeight(int height) const; + +protected: + void resizeEvent(QResizeEvent* resizeEvent) override; + +private: + void rescalePixmap(const QSize& size); + + QPixmap mUnscaledPixmap; + QLabel copySizeLabel; +}; diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE new file mode 100644 index 0000000..e2874c2 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) unknown thibdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp new file mode 100644 index 0000000..d686c58 --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp @@ -0,0 +1,165 @@ +#include "checkboxwordwrap.h" + +#include +#include + +CheckBoxWordWrap::CheckBoxWordWrap(QWidget *parent) + : QCheckBox (parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(this)) +{ + init(); +} + +CheckBoxWordWrap::CheckBoxWordWrap(const QString &text, QWidget *parent) + : QCheckBox (parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(text, this)) +{ + init(); +} + +CheckBoxWordWrap::~CheckBoxWordWrap() +{ + delete m_label; + delete m_hMainLayout; +} + +bool CheckBoxWordWrap::isWordWrap() const +{ + return m_label->wordWrap(); +} + +void CheckBoxWordWrap::setWordWrap(bool wordwrap) +{ + m_label->setWordWrap(wordwrap); +} + +QString CheckBoxWordWrap::text() const +{ + return m_label->text(); +} + +void CheckBoxWordWrap::setText(const QString &text) +{ + m_label->setText(text); +} + +QSize CheckBoxWordWrap::sizeHint() const +{ + QFontMetrics fm(m_label->font()); + QRect r = m_label->rect(); + r.setLeft(r.left()+m_label->indent()+separation); + QRect bRect = fm.boundingRect(r, int(Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap), m_label->text()); + QSize ret = QSize(QWidget::sizeHint().width(), bRect.height()); + return ret; +} + +void CheckBoxWordWrap::labelIsClicked() +{ + setChecked(!isChecked()); +} + +void CheckBoxWordWrap::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + updateGeometry(); +} + +void CheckBoxWordWrap::init() +{ + setLayout(m_hMainLayout); + QStyleOptionButton opt; + initStyleOption(&opt); + int indicatorW = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorWidth, &opt, this); + // Useless in our case, we only need the indicator width + //int indicatorH = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorHeight, &opt, this); + m_hMainLayout->setContentsMargins(0, 0, 0, 0); + m_hMainLayout->addWidget(m_label); + m_label->setIndent(indicatorW+separation); + m_label->setWordWrap(true); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + connect(m_label, SIGNAL(clicked()), this, SLOT(labelIsClicked())); +} + + +RadioButtonWordWrap::RadioButtonWordWrap(QWidget* parent) + : QRadioButton(parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(this)) +{ + init(); +} + +RadioButtonWordWrap::RadioButtonWordWrap(const QString& text, QWidget* parent) + : QRadioButton(parent) + , m_hMainLayout(new QHBoxLayout(this)) + , m_label(new ClickableLabel(text, this)) +{ + init(); +} + +RadioButtonWordWrap::~RadioButtonWordWrap() +{ + delete m_label; + delete m_hMainLayout; +} + +bool RadioButtonWordWrap::isWordWrap() const +{ + return m_label->wordWrap(); +} + +void RadioButtonWordWrap::setWordWrap(bool wordwrap) +{ + m_label->setWordWrap(wordwrap); +} + +QString RadioButtonWordWrap::text() const +{ + return m_label->text(); +} + +void RadioButtonWordWrap::setText(const QString& text) +{ + m_label->setText(text); +} + +QSize RadioButtonWordWrap::sizeHint() const +{ + QFontMetrics fm(m_label->font()); + QRect r = m_label->rect(); + r.setLeft(r.left() + m_label->indent() + separation); + QRect bRect = fm.boundingRect(r, int(Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap), m_label->text()); + QSize ret = QSize(QWidget::sizeHint().width(), bRect.height()); + return ret; +} + +void RadioButtonWordWrap::labelIsClicked() +{ + setChecked(!isChecked()); +} + +void RadioButtonWordWrap::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + updateGeometry(); +} + +void RadioButtonWordWrap::init() +{ + setLayout(m_hMainLayout); + QStyleOptionButton opt; + initStyleOption(&opt); + int indicatorW = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorWidth, &opt, this); + // Useless in our case, we only need the indicator width + //int indicatorH = style()->pixelMetric(QStyle::PixelMetric::PM_IndicatorHeight, &opt, this); + m_hMainLayout->setContentsMargins(0, 0, 0, 0); + m_hMainLayout->addWidget(m_label); + m_label->setIndent(indicatorW + separation); + m_label->setWordWrap(true); + + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); + connect(m_label, SIGNAL(clicked()), this, SLOT(labelIsClicked())); +} diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h new file mode 100644 index 0000000..dc4d30c --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h @@ -0,0 +1,75 @@ +#ifndef CHECKBOXWORDWRAP_H +#define CHECKBOXWORDWRAP_H + +#include +#include +#include + +#include "clickablelabel.h" + +/** + * @author thibdev + */ +class CheckBoxWordWrap : public QCheckBox +{ + Q_OBJECT + + Q_PROPERTY(bool wordwrap READ isWordWrap WRITE setWordWrap) + Q_PROPERTY(QString text READ text WRITE setText) + +public: + CheckBoxWordWrap(QWidget *parent = Q_NULLPTR); + CheckBoxWordWrap(const QString &text, QWidget *parent = Q_NULLPTR); + ~CheckBoxWordWrap(); + bool isWordWrap() const; + void setWordWrap(bool wordwrap); + QString text() const; + void setText(const QString &text); + QSize sizeHint() const override; + +private slots: + void labelIsClicked(); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + void init(); + const int separation = 5; + QHBoxLayout *m_hMainLayout; + ClickableLabel *m_label; + +}; + +class RadioButtonWordWrap : public QRadioButton +{ + Q_OBJECT + + Q_PROPERTY(bool wordwrap READ isWordWrap WRITE setWordWrap) + Q_PROPERTY(QString text READ text WRITE setText) + +public: + RadioButtonWordWrap(QWidget* parent = Q_NULLPTR); + RadioButtonWordWrap(const QString& text, QWidget* parent = Q_NULLPTR); + ~RadioButtonWordWrap(); + bool isWordWrap() const; + void setWordWrap(bool wordwrap); + QString text() const; + void setText(const QString& text); + QSize sizeHint() const override; + +private slots: + void labelIsClicked(); + +protected: + void resizeEvent(QResizeEvent* event) override; + +private: + void init(); + const int separation = 5; + QHBoxLayout* m_hMainLayout; + ClickableLabel* m_label; + +}; + +#endif diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp new file mode 100644 index 0000000..6f8d47f --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp @@ -0,0 +1,24 @@ +#include "clickablelabel.h" + +ClickableLabel::ClickableLabel(QWidget *parent, Qt::WindowFlags f) + : QLabel(parent, f) +{ + +} + +ClickableLabel::ClickableLabel(const QString &text, QWidget *parent, Qt::WindowFlags f) + : QLabel(text, parent, f) +{ + +} + +ClickableLabel::~ClickableLabel() +{ + +} + +void ClickableLabel::mouseReleaseEvent(QMouseEvent *event) +{ + Q_UNUSED(event); + emit clicked(); +} diff --git a/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h new file mode 100644 index 0000000..c3ee22b --- /dev/null +++ b/libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h @@ -0,0 +1,27 @@ +#ifndef CLICKABLELABEL_H +#define CLICKABLELABEL_H + +#include +#include + +/** + * @brief The ClickableLabel class + * https://wiki.qt.io/Clickable_QLabel + */ +class ClickableLabel : public QLabel +{ + Q_OBJECT +public: + explicit ClickableLabel(QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + explicit ClickableLabel(const QString &text, QWidget *parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); + ~ClickableLabel() override; + +signals: + void clicked(); + +protected: + void mouseReleaseEvent(QMouseEvent *event) override; + +}; + +#endif // CLICKABLELABEL_H diff --git a/libs/installer_omod/src/oldstuff/folder.md b/libs/installer_omod/src/oldstuff/folder.md new file mode 100644 index 0000000..3b5009a --- /dev/null +++ b/libs/installer_omod/src/oldstuff/folder.md @@ -0,0 +1 @@ +This is stuff I'm just lifting from my original attempt at an OMOD installer. It's probably terrible and I'm barely going to fix it. I expect and want other MO2 devs to rework/replace it with something better. -- cgit v1.3.1