aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_omod/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/installer_omod/src
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/installer_omod/src')
-rw-r--r--libs/installer_omod/src/CMakeLists.txt67
-rw-r--r--libs/installer_omod/src/DummyCSFile.cs16
-rw-r--r--libs/installer_omod/src/MessageBoxHelper.cpp61
-rw-r--r--libs/installer_omod/src/MessageBoxHelper.h37
-rw-r--r--libs/installer_omod/src/OMODFrameworkWrapper.cpp771
-rw-r--r--libs/installer_omod/src/OMODFrameworkWrapper.h69
-rw-r--r--libs/installer_omod/src/QObject_unique_ptr.h20
-rw-r--r--libs/installer_omod/src/implementations/CodeProgress.cpp85
-rw-r--r--libs/installer_omod/src/implementations/CodeProgress.h59
-rw-r--r--libs/installer_omod/src/implementations/Logger.cpp45
-rw-r--r--libs/installer_omod/src/implementations/Logger.h17
-rw-r--r--libs/installer_omod/src/implementations/ScriptFunctions.cpp320
-rw-r--r--libs/installer_omod/src/implementations/ScriptFunctions.h122
-rw-r--r--libs/installer_omod/src/installerOmod.cpp204
-rw-r--r--libs/installer_omod/src/installerOmod.h75
-rw-r--r--libs/installer_omod/src/installer_omod_en.ts159
-rw-r--r--libs/installer_omod/src/interop/QtDotNetConverters.cpp17
-rw-r--r--libs/installer_omod/src/interop/QtDotNetConverters.h9
-rw-r--r--libs/installer_omod/src/interop/StdDotNetConverters.cpp35
-rw-r--r--libs/installer_omod/src/interop/StdDotNetConverters.h13
-rw-r--r--libs/installer_omod/src/newstuff/namedialog.cpp37
-rw-r--r--libs/installer_omod/src/newstuff/namedialog.h28
-rw-r--r--libs/installer_omod/src/newstuff/namedialog.ui89
-rw-r--r--libs/installer_omod/src/newstuff/rtfPopup.cpp33
-rw-r--r--libs/installer_omod/src/newstuff/rtfPopup.h11
-rw-r--r--libs/installer_omod/src/oldstuff/DialogSelect.cpp301
-rw-r--r--libs/installer_omod/src/oldstuff/DialogSelect.h46
-rw-r--r--libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/LICENSE21
-rw-r--r--libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.cpp165
-rw-r--r--libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/checkboxwordwrap.h75
-rw-r--r--libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.cpp24
-rw-r--r--libs/installer_omod/src/oldstuff/MIT-licencedCodeToDoStuff/clickablelabel.h27
-rw-r--r--libs/installer_omod/src/oldstuff/folder.md1
33 files changed, 3059 insertions, 0 deletions
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 "$<TARGET_FILE_DIR:dummy_cs_project>", 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
+ "$<TARGET_FILE_DIR:${PROJECT_NAME}>/OMODFramework.dll"
+ "$<TARGET_FILE_DIR:${PROJECT_NAME}>/OMODFramework.Scripting.dll"
+ "$<TARGET_FILE_DIR:${PROJECT_NAME}>/ICSharpCode.SharpZipLib.dll"
+ "$<TARGET_FILE_DIR:${PROJECT_NAME}>/System.Drawing.Common.dll"
+ "$<TARGET_FILE_DIR:${PROJECT_NAME}>/RtfPipe.dll"
+ DESTINATION bin/plugins/installer_omod/
+)
+install(
+ FILES "$<TARGET_PDB_FILE_DIR:${PROJECT_NAME}>/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 <QApplication>
+
+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 <QMessageBox>
+
+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 <algorithm>
+#include <array>
+#include <filesystem>
+
+#include <QMessageBox>
+#include <QTemporaryDir>
+#include <QProgressDialog>
+
+#include <uibase/imodinterface.h>
+#include <uibase/imodlist.h>
+#include <uibase/iplugingame.h>
+#include <uibase/ipluginlist.h>
+#include <uibase/game_features/igamefeatures.h>
+#include <uibase/log.h>
+#include <uibase/utility.h>
+#include <uibase/registry.h>
+
+#include <uibase/game_features/dataarchives.h>
+#include <uibase/game_features/gameplugins.h>
+
+#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<QProgressDialog>())
+{
+ 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<QString>& 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<QString>* 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<QString>& 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 <typename T>
+class scope_guard
+{
+public:
+ scope_guard(T onExit) : mOnExit(onExit) {}
+ ~scope_guard() { mOnExit(); }
+private:
+ T mOnExit;
+};
+
+OMODFrameworkWrapper::EInstallResult OMODFrameworkWrapper::install(MOBase::GuessedValue<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID)
+{
+ try
+ {
+ QObject_unique_ptr<MessageBoxHelper> messageBoxHelper = make_unique<MessageBoxHelper>();
+
+ 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<System::String^>^ activeBSAs = gcnew System::Collections::Generic::List<System::String^>();
+ {
+ // Hopefully some other part of MO2 also knows this and this can be refactored
+ auto dataArchives = mMoInfo->gameFeatures()->gameFeature<MOBase::DataArchives>();
+ // 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<int, QString> 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<QString, QMap<QString, QString>> 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<System::String^>^ installedPlugins = gcnew System::Collections::Generic::HashSet<System::String^>(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<QString, int> 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<CodeProgress^>(OMODFramework::Framework::Settings->CodeProgress)->setParentWidget(mParentWidget);
+}
+
+const std::array<QString, 3> 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<MOBase::GamePlugins>()->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
+ <ul><li>%2</li></ul> 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:<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.")
+ .arg(omodName).arg(registeredBSAs.join("</li><li>"))
+ );
+ }
+ }
+
+ 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<QString>& 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<QString>& modName)
+{
+ modInterfaceOut = mMoInfo->createMod(modName);
+}
+
+void OMODFrameworkWrapper::displayReadmeSlot(const QString& modName, const QString& readme)
+{
+ if (QMessageBox::question(mParentWidget, tr("Display Readme?"),
+ //: <br> is a line break. Translators can remove it if it makes things clearer.
+ tr("The Readme may explain installation options. Display it?<br>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 <QCoreApplication>
+#include <QProgressDialog>
+#include <QStack>
+
+#include <uibase/iplugininstaller.h>
+
+#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<class T>
+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<QString>& modName, QString gameName, const QString& archiveName, const QString& version, int nexusID);
+
+ EInstallResult install(MOBase::GuessedValue<QString>& 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<QString>& modName);
+
+ void createMod(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue<QString>& modName);
+
+ void displayReadme(const QString& modName, const QString& readme);
+
+ void showWaitDialog(QString message);
+ void hideWaitDialog();
+
+protected slots:
+ void pickModNameSlot(bool& successOut, MOBase::GuessedValue<QString>& modName);
+ void createModSlot(MOBase::IModInterface*& modInterfaceOut, MOBase::GuessedValue<QString>& 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<QString> mTempPathStack;
+
+ QObject_unique_ptr<QProgressDialog> 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 <QObject>
+
+static inline void deleteQObjectLater(QObject* qObject) { qObject->deleteLater(); }
+
+template <typename T>
+using QObject_unique_ptr = std::unique_ptr<T, decltype(&deleteQObjectLater)>;
+
+template <typename T, typename... Args>
+QObject_unique_ptr<T> make_unique(Args&&... args)
+{
+ return QObject_unique_ptr<T>(new T(std::forward<Args>(args)...), &deleteQObjectLater);
+}
+
+template <typename T>
+QObject_unique_ptr<T> make_nullptr()
+{
+ return QObject_unique_ptr<T>(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 <QApplication>
+
+#include "CodeProgress.h"
+
+CodeProgressHelper::CodeProgressHelper(QWidget* parentWidget) : mParentWidget{ parentWidget }, mProgressDialog(make_nullptr<QProgressDialog>()) {
+ 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 <QObject>
+#include <QWidget>
+#include <QProgressDialog>
+
+#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<QProgressDialog> 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 <uibase/log.h>
+
+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 <QApplication>
+#include <QDir>
+#include <QGridLayout>
+#include <QInputDialog>
+#include <QImageReader>
+#include <QLabel>
+#include <QMessageBox>
+#include <QScreen>
+
+#include <uibase/iplugingame.h>
+#include <uibase/ipluginlist.h>
+#include <uibase/log.h>
+
+#include "../interop/QtDotNetConverters.h"
+#include "../newstuff/rtfPopup.h"
+#include "../oldstuff/DialogSelect.h"
+
+ScriptFunctionsHelper::ScriptFunctionsHelper() : mMessageBoxHelper(make_unique<MessageBoxHelper>())
+{
+ 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<QVector<int>> ScriptFunctionsHelper::DialogSelect(QWidget* parent, const QString& title, const QVector<QString>& items, const QVector<QString>& descriptions, const QVector<QString>& pixmaps, bool multiSelect)
+{
+ std::optional<QVector<int>> 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<int>(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<QVector<int>>& resultOut, QWidget* parent, const QString& title, const QVector<QString>& items,
+ const QVector<QString>& descriptions, const QVector<QString>& 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<int>^ ScriptFunctions::Select(System::Collections::Generic::List<System::String^>^ items,
+ System::String^ title,
+ bool isMultiSelect,
+ System::Collections::Generic::List<System::String^>^ previews,
+ System::Collections::Generic::List<System::String^>^ descriptions)
+{
+ QVector<QString> qItems;
+ qItems.reserve(items ? items->Count : 0);
+ QVector<QString> qPreviews;
+ qPreviews.reserve(previews ? previews->Count : 0);
+ QVector<QString> 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<QVector<int>> qResponse = mHelper->DialogSelect(mParentWidget, toQString(title), qItems, qDescriptions, qPreviews, isMultiSelect);
+ if (!qResponse.has_value())
+ return nullptr;
+
+ System::Collections::Generic::List<int>^ response = gcnew System::Collections::Generic::List<int>(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<OMODFramework::Scripting::ScriptESP>^ ScriptFunctions::GetESPs()
+{
+ QStringList plugins = mMoInfo->pluginList()->pluginNames();
+ System::Collections::Generic::List<OMODFramework::Scripting::ScriptESP>^ pluginList = gcnew System::Collections::Generic::List<OMODFramework::Scripting::ScriptESP>(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<System::String^>^ 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<unsigned char, 1>^ 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<unsigned char, 1>^ 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<unsigned char, 1>^ 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 <optional>
+
+#include <QWidget>
+
+#include <uibase/imoinfo.h>
+
+#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<typename... Args> auto critical(Args... args) { return mMessageBoxHelper->critical(args...); }
+ template<typename... Args> auto information(Args... args) { return mMessageBoxHelper->information(args...); }
+ template<typename... Args> auto question(Args... args) { return mMessageBoxHelper->question(args...); }
+ template<typename... Args> auto warning(Args... args) { return mMessageBoxHelper->warning(args...); }
+
+ std::optional<QVector<int>> DialogSelect(QWidget* parent, const QString& title, const QVector<QString>& items,
+ const QVector<QString>& descriptions, const QVector<QString>& 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<QVector<int>>& resultOut, QWidget* parent, const QString& title, const QVector<QString>& items,
+ const QVector<QString>& descriptions, const QVector<QString>& 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<QVector<int>>& resultOut, QWidget* parent, const QString& title, const QVector<QString>& items,
+ const QVector<QString>& descriptions, const QVector<QString>& 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<MessageBoxHelper> 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<int>^ Select(System::Collections::Generic::List<System::String^>^ items, System::String^ title, bool isMultiSelect, System::Collections::Generic::List<System::String^>^ previews, System::Collections::Generic::List<System::String^>^ 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<OMODFramework::Scripting::ScriptESP>^ GetESPs();
+
+ virtual System::Collections::Generic::IEnumerable<System::String^>^ GetActiveOMODNames();
+
+ virtual cli::array<unsigned char, 1>^ ReadExistingDataFile(System::String^ file);
+
+ virtual cli::array<unsigned char, 1>^ GetDataFileFromBSA(System::String^ file);
+
+ virtual cli::array<unsigned char, 1>^ 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 <QRegularExpression>
+#include <QTemporaryFile>
+
+#include <uibase/iplugingame.h>
+#include <uibase/log.h>
+
+#include "OMODFrameworkWrapper.h"
+
+InstallerOMOD::InstallerOMOD() : mMoInfo(nullptr), mOmodFrameworkWrapper(nullptr)
+{
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// IPlugin
+
+bool InstallerOMOD::init(MOBase::IOrganizer* moInfo)
+{
+ mMoInfo = moInfo;
+ mOmodFrameworkWrapper = std::make_unique<OMODFrameworkWrapper>(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<std::shared_ptr<const MOBase::IPluginRequirement>> InstallerOMOD::requirements() const
+{
+ return { Requirements::gameDependency("Oblivion") };
+}
+
+QList<MOBase::PluginSetting> 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<const MOBase::IFileTree> 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<QString> InstallerOMOD::supportedExtensions() const
+{
+ return { "omod" };
+}
+
+InstallerOMOD::EInstallResult InstallerOMOD::install(MOBase::GuessedValue<QString>& 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<QByteArray, QByteArray> 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<QTemporaryFile> outputFile = std::make_unique<QTemporaryFile>();
+ 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 <QTemporaryFile>
+
+#include <uibase/iplugininstallercustom.h>
+#include <uibase/ipluginfilemapper.h>
+
+#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<std::shared_ptr<const MOBase::IPluginRequirement>> requirements() const override;
+
+ QList<MOBase::PluginSetting> 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<const MOBase::IFileTree> tree) const override;
+
+ void setParentWidget(QWidget* parent) override;
+
+ // IPluginInstallerCustom
+
+ bool isArchiveSupported(const QString& archiveName) const override;
+
+ std::set<QString> supportedExtensions() const override;
+
+ EInstallResult install(MOBase::GuessedValue<QString>& 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<OMODFrameworkWrapper> mOmodFrameworkWrapper;
+ std::map<QString, std::unique_ptr<QTemporaryFile>> 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>InstallerOMOD</name>
+ <message>
+ <location filename="installerOmod.cpp" line="34"/>
+ <source>Omod Installer</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="installerOmod.cpp" line="44"/>
+ <source>Installer for Omod files (including scripted ones)</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>NameDialog</name>
+ <message>
+ <location filename="newstuff/namedialog.ui" line="14"/>
+ <source>Pick mod name</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="newstuff/namedialog.ui" line="22"/>
+ <source>Name</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="newstuff/namedialog.ui" line="43"/>
+ <location filename="newstuff/namedialog.ui" line="46"/>
+ <source>Manual install is unavailable for OMODs.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="newstuff/namedialog.ui" line="49"/>
+ <source>Manual</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="newstuff/namedialog.ui" line="69"/>
+ <source>OK</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="newstuff/namedialog.ui" line="79"/>
+ <source>Cancel</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>OMODFrameworkWrapper</name>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="365"/>
+ <source>%1 wants to change [%2] %3 from &quot;%4&quot; to &quot;%5&quot;</source>
+ <extracomment>%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.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="374"/>
+ <source>%1 wants to set [%2] %3 to &quot;%4&quot;</source>
+ <extracomment>%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.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="377"/>
+ <source>Update INI?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="468"/>
+ <source>%1 has data for %2, but Mod Organizer 2 doesn&apos;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&apos;t find any OMODs that actually did this, and we need to know that they exist.</source>
+ <extracomment>%1 is the mod name %2 is the name of a field in the OMOD&apos;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.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="471"/>
+ <source>Mod Organizer 2 can&apos;t completely install this OMOD.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="553"/>
+ <source>Activate mod?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="557"/>
+ <source>%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?</source>
+ <extracomment>%1 is the left-pane mod name. %2 is the name from the metadata of an OMOD.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="587"/>
+ <source>%1 wants to activate %2. Do you want to do so?</source>
+ <extracomment>%1 is the mod name. %2 is the plugin name.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="589"/>
+ <location filename="OMODFrameworkWrapper.cpp" line="632"/>
+ <source>Activate plugin?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="601"/>
+ <source>OMOD wants to activate missing plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="601"/>
+ <source>An OMOD wants to activate a missing plugin. This shouldn&apos;t be possible. Please report this to a MO2 developer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="630"/>
+ <source>%1 installed %2, but doesn&apos;t activate it by default. Do you want to activate it anyway?</source>
+ <extracomment>%1 is the mod name. %2 is the plugin name.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="644"/>
+ <source>OMOD claimed to install missing plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="644"/>
+ <source>An OMOD has activation settings for a missing plugin. This shouldn&apos;t be possible. Please report this to a MO2 developer.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="666"/>
+ <source>Register BSAs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="670"/>
+ <source>%1 wants to register the following BSA archives, but Mod Organizer 2 can&apos;t do that yet due to technical limitations:&lt;ul&gt;&lt;li&gt;%2&lt;/li&gt;&lt;/ul&gt;For now, your options include adding the BSA names to &lt;code&gt;sResourceArchiveList&lt;/code&gt; in the game INI, creating a dummy ESP with the same name, or extracting the BSA, all of which have drawbacks.</source>
+ <extracomment>%1 is the OMOD name &lt;ul&gt;&lt;li&gt;%2&lt;/li&gt;&lt;/ul&gt; becomes a list of BSA files</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="745"/>
+ <source>Display Readme?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="747"/>
+ <source>The Readme may explain installation options. Display it?&lt;br&gt;It will remain visible until you close it.</source>
+ <extracomment>&lt;br&gt; is a line break. Translators can remove it if it makes things clearer.</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="OMODFrameworkWrapper.cpp" line="753"/>
+ <source>%1 Readme</source>
+ <extracomment>%1 is the mod name</extracomment>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
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 <string>
+
+#include <msclr\marshal_cppstd.h>
+
+QString toQString(System::String^ string)
+{
+ if (string)
+ return QString::fromStdWString(msclr::interop::marshal_as<std::wstring>(string));
+ return QString();
+}
+
+System::String^ toDotNetString(const QString& qString)
+{
+ return qString.isNull() ? nullptr : msclr::interop::marshal_as<System::String^>(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>
+
+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 <stdexcept>
+
+#include <msclr\marshal_cppstd.h>
+
+std::wstring toWString(System::String^ string)
+{
+ if (string)
+ return msclr::interop::marshal_as<std::wstring>(string);
+ return L"null .NET string";
+}
+
+System::String^ toDotNetString(const std::wstring& wString)
+{
+ return msclr::interop::marshal_as<System::String^>(wString);
+}
+
+std::string toUTF8String(System::String^ string)
+{
+ if (string)
+ {
+ array<unsigned char>^ 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 <string>
+
+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 <QCompleter>
+
+NameDialog::NameDialog(const MOBase::GuessedValue<QString>& 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 <QDialog>
+
+#include <uibase/guessedvalue.h>
+
+#include "ui_namedialog.h"
+
+class NameDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ NameDialog(const MOBase::GuessedValue<QString>& 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>NameDialog</class>
+ <widget class="QDialog" name="NameDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>83</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Pick mod name</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,1">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Name</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="nameCombo">
+ <property name="editable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QPushButton" name="manualBtn">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="toolTip">
+ <string>Manual install is unavailable for OMODs.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Manual install is unavailable for OMODs.</string>
+ </property>
+ <property name="text">
+ <string>Manual</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="okBtn">
+ <property name="text">
+ <string>OK</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cancelBtn">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
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 <QGridLayout>
+#include <QLabel>
+#include <QRegularExpression>
+#include <QScrollArea>
+#include <QTextDocument>
+
+#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((?<!(?:href="))((?:(?:https?|ftp|file)://|www\.|ftp\.)(?:\([-A-Z0-9+@#/%=~_|$?!:,.]|(?:&amp;)*\)|[-A-Z0-9+@#/%=~_|$?!:,.]|(?:&amp;))*(?:\([-A-Z0-9+@#/%=~+|$?!:,.]|(?:&amp;)*\)|[A-Z0-9+@#/%=~_|$]|(?:&amp;))))REGEX", QRegularExpression::CaseInsensitiveOption | QRegularExpression::MultilineOption);
+ text.replace(urlFinder, R"(<a href="\1">\1</a>)");
+
+ 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 <QDialog>
+
+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 <functional>
+
+#include <QCheckBox>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QDebug>
+#include <QImageReader>
+#include <QPlainTextEdit>
+#include <QRadioButton>
+#include <QScrollArea>
+#include <QSplitter>
+#include <QStackedWidget>
+#include <QVBoxLayout>
+
+#include "MIT-licencedCodeToDoStuff/checkboxwordwrap.h"
+
+// there is no hover signal, the only way to know is to override enterEvent()
+//
+template <class T, typename... Args>
+class HoverableWidget : public T
+{
+public:
+ std::function<void ()> onHover;
+
+ HoverableWidget(Args... args, std::function<void ()> 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<QVector<int>> DialogSelect(
+ QWidget* parent, const QString& title, const QVector<QString>& items,
+ const QVector<QString>& descriptions, const QVector<QString>& 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<QAbstractButton*> itemButtons;
+
+ // for each item
+ for (int i=0; i<items.size(); ++i) {
+ QAbstractButton* w = nullptr;
+
+ QString labelText = items[i];
+ bool checked = false;
+
+ // THIS IS IMPORTANT WHEN REPLACING THIS WITH BETTER CODE. OMODS HAVE A PIPE AT THE START OF ANY OPTIONS SELECTED BY DEFAULT.
+ if (labelText.startsWith('|'))
+ {
+ labelText = labelText.remove(0, 1);
+ checked = true;
+ }
+
+ if (multiSelect) {
+ w = new HoverableWidget<CheckBoxWordWrap, const QString&, QWidget*>(labelText, rightInner, [=]{ onHover(i); });
+ } else {
+ w = new HoverableWidget<RadioButtonWordWrap, const QString&, QWidget*>(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<int> selection;
+ for (int i=0; i<itemButtons.size(); ++i) {
+ if (itemButtons[i]->isChecked()) {
+ 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 <optional>
+
+#include <QLabel>
+#include <QVector>
+#include <QWidget>
+
+std::optional<QVector<int>> DialogSelect(QWidget* parent, const QString& title, const QVector<QString>& items,
+ const QVector<QString>& descriptions, const QVector<QString>& 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 <QStyle>
+#include <QStyleOptionButton>
+
+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 <QCheckBox>
+#include <QHBoxLayout>
+#include <QRadioButton>
+
+#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 <QLabel>
+#include <QMouseEvent>
+
+/**
+ * @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.