From cf19f33df30360a44315af020045d6acd599aebc Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 8 Dec 2025 14:35:13 +1000 Subject: [PATCH 01/30] redone from scratch for dev --- features/Weather Editor/CORE | 0 .../Shaders/Features/WeatherEditor.ini | 2 + src/Deferred.cpp | 23 +- src/Feature.cpp | 4 +- src/Feature.h | 16 + src/Features/WeatherEditor.cpp | 344 ++++++++++ src/Features/WeatherEditor.h | 47 ++ src/Globals.cpp | 4 + src/Globals.h | 4 + src/Menu.cpp | 6 + src/State.cpp | 7 + src/Util.h | 1 + src/Utils/Form.cpp | 22 + src/Utils/Form.h | 7 + src/Utils/Serialize.cpp | 27 + src/Utils/Serialize.h | 3 + src/WeatherEditor/EditorWindow.cpp | 639 +++++++++++++++++ src/WeatherEditor/EditorWindow.h | 80 +++ .../Weather/LightingTemplateWidget.cpp | 26 + .../Weather/LightingTemplateWidget.h | 21 + src/WeatherEditor/Weather/WeatherWidget.cpp | 640 ++++++++++++++++++ src/WeatherEditor/Weather/WeatherWidget.h | 92 +++ .../Weather/WorldSpaceWidget.cpp | 28 + src/WeatherEditor/Weather/WorldSpaceWidget.h | 28 + src/WeatherEditor/WeatherUtils.cpp | 152 +++++ src/WeatherEditor/WeatherUtils.h | 23 + src/WeatherEditor/Widget.cpp | 98 +++ src/WeatherEditor/Widget.h | 72 ++ src/WeatherManager.cpp | 216 ++++++ src/WeatherManager.h | 59 ++ src/XSEPlugin.cpp | 3 + 31 files changed, 2688 insertions(+), 6 deletions(-) create mode 100644 features/Weather Editor/CORE create mode 100644 features/Weather Editor/Shaders/Features/WeatherEditor.ini create mode 100644 src/Features/WeatherEditor.cpp create mode 100644 src/Features/WeatherEditor.h create mode 100644 src/Utils/Form.cpp create mode 100644 src/Utils/Form.h create mode 100644 src/WeatherEditor/EditorWindow.cpp create mode 100644 src/WeatherEditor/EditorWindow.h create mode 100644 src/WeatherEditor/Weather/LightingTemplateWidget.cpp create mode 100644 src/WeatherEditor/Weather/LightingTemplateWidget.h create mode 100644 src/WeatherEditor/Weather/WeatherWidget.cpp create mode 100644 src/WeatherEditor/Weather/WeatherWidget.h create mode 100644 src/WeatherEditor/Weather/WorldSpaceWidget.cpp create mode 100644 src/WeatherEditor/Weather/WorldSpaceWidget.h create mode 100644 src/WeatherEditor/WeatherUtils.cpp create mode 100644 src/WeatherEditor/WeatherUtils.h create mode 100644 src/WeatherEditor/Widget.cpp create mode 100644 src/WeatherEditor/Widget.h create mode 100644 src/WeatherManager.cpp create mode 100644 src/WeatherManager.h diff --git a/features/Weather Editor/CORE b/features/Weather Editor/CORE new file mode 100644 index 0000000000..e69de29bb2 diff --git a/features/Weather Editor/Shaders/Features/WeatherEditor.ini b/features/Weather Editor/Shaders/Features/WeatherEditor.ini new file mode 100644 index 0000000000..19f01444dc --- /dev/null +++ b/features/Weather Editor/Shaders/Features/WeatherEditor.ini @@ -0,0 +1,2 @@ +[Info] +Version = 1-0-0 \ No newline at end of file diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 06a063f2cb..0c7fe1394b 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -13,6 +13,7 @@ #include "Features/SubsurfaceScattering.h" #include "Features/TerrainBlending.h" #include "Features/Upscaling.h" +#include "Features/WeatherEditor.h" #include "Hooks.h" @@ -298,6 +299,10 @@ void Deferred::PrepassPasses() void Deferred::StartDeferred() { + WeatherEditor::GetSingleton()->Bind(); + + if (!globals::state->inWorld) + return; globals::state->UpdateSharedData(true, false); auto shadowState = globals::game::shadowState; @@ -413,14 +418,16 @@ void Deferred::DeferredPasses() dynamicCubemaps.UpdateCubemap(); auto& terrainBlending = globals::features::terrainBlending; - + auto& weatherEditor = globals::features::weatherEditor; auto& ibl = globals::features::ibl; + auto shadowMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kSHADOW_MASK]; + // Deferred Composite { TracyD3D11Zone(globals::state->tracyCtx, "Deferred Composite"); - ID3D11ShaderResourceView* srvs[16]{ + ID3D11ShaderResourceView* srvs[17]{ specular.SRV, albedo.SRV, normalRoughness.SRV, @@ -435,12 +442,15 @@ void Deferred::DeferredPasses() ssgi_hq_spec ? nullptr : ssgi_y, ssgi_hq_spec ? nullptr : ssgi_cocg, ssgi_hq_spec ? ssgi_gi_spec : nullptr, + weatherEditor.loaded ? weatherEditor.diffuseIBLTexture->srv.get() : nullptr, ibl.loaded ? ibl.diffuseIBLTexture->srv.get() : nullptr, ibl.loaded ? ibl.diffuseSkyIBLTexture->srv.get() : nullptr, }; - if (dynamicCubemaps.loaded) - context->CSSetSamplers(0, 1, &linearSampler); + ID3D11SamplerState* samplers[]{ + dynamicCubemaps.loaded ? linearSampler : nullptr, + }; + context->CSSetSamplers(0, ARRAYSIZE(samplers), samplers); context->CSSetShaderResources(0, ARRAYSIZE(srvs), srvs); @@ -455,7 +465,7 @@ void Deferred::DeferredPasses() // Clear { - ID3D11ShaderResourceView* views[16]{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; + ID3D11ShaderResourceView* views[17]{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); ID3D11UnorderedAccessView* uavs[3]{ nullptr, nullptr, nullptr }; @@ -464,6 +474,9 @@ void Deferred::DeferredPasses() ID3D11Buffer* buffers[1] = { nullptr }; context->CSSetConstantBuffers(12, 1, buffers); + ID3D11SamplerState* samplers[2]{ nullptr, nullptr }; + context->CSSetSamplers(0, ARRAYSIZE(samplers), samplers); + context->CSSetShader(nullptr, nullptr, 0); } diff --git a/src/Feature.cpp b/src/Feature.cpp index e3facbed1d..34361b07de 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -29,6 +29,7 @@ #include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/WaterEffects.h" +#include "Features/WeatherEditor.h" #include "Features/WeatherPicker.h" #include "Features/WetnessEffects.h" #include "Menu.h" @@ -227,7 +228,8 @@ const std::vector& Feature::GetFeatureList() &globals::features::ibl, &globals::features::extendedTranslucency, &globals::features::upscaling, - &globals::features::renderDoc + &globals::features::renderDoc, + &globals::features::weatherEditor }; if (REL::Module::IsVR()) { diff --git a/src/Feature.h b/src/Feature.h index 2f0367331e..0a9d8e3671 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -128,6 +128,22 @@ struct Feature */ virtual WeatherAnalysisConfig GetWeatherAnalysisConfig() const { return {}; } + /** + * @brief Indicates whether this feature supports per-weather settings + * @return True if the feature has weather-specific settings that should be interpolated + */ + virtual bool SupportsWeather() const { return false; } + + /** + * @brief Updates feature settings based on weather transition + * Called by WeatherManager when weather changes or transitions occur. + * Features should lerp between currWeather and nextWeather settings using lerpFactor. + * @param currWeather Settings from the current weather (may be empty if no settings exist) + * @param nextWeather Settings from the transitioning weather (may be empty) + * @param lerpFactor Blend factor between weathers (0.0 = fully nextWeather, 1.0 = fully currWeather) + */ + virtual void UpdateSettingsFromWeathers(const json& currWeather, const json& nextWeather, float lerpFactor) {} + virtual bool ValidateCache(CSimpleIniA& a_ini); virtual void WriteDiskCacheInfo(CSimpleIniA& a_ini); virtual void ClearShaderCache() {} diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp new file mode 100644 index 0000000000..72fd300082 --- /dev/null +++ b/src/Features/WeatherEditor.cpp @@ -0,0 +1,344 @@ +#include "Weather.h" + +#include "Deferred.h" +#include "State.h" +#include "Util.h" +#include "WeatherManager.h" + +#include "Editor/EditorWindow.h" + +int8_t LerpInt8_t(const int8_t oldValue, const int8_t newVal, const float lerpValue) +{ + int lerpedValue = (int)std::lerp(oldValue, newVal, lerpValue); + return (int8_t)std::clamp(lerpedValue, -128, 127); +} + +uint8_t LerpUint8_t(const uint8_t oldValue, const uint8_t newVal, const float lerpValue) +{ + int lerpedValue = (int)std::lerp(oldValue, newVal, lerpValue); + return (uint8_t)std::clamp(lerpedValue, 0, 255); +} + +void LerpColor(const RE::TESWeather::Data::Color3& oldColor, RE::TESWeather::Data::Color3& newColor, const float changePct) +{ + newColor.red = (int8_t)std::lerp(-128, 127, changePct); + newColor.green = (int8_t)std::lerp(oldColor.green, newColor.green, changePct); + newColor.blue = LerpInt8_t(oldColor.blue, newColor.blue, changePct); +} + +void LerpColor(const RE::Color& oldColor, RE::Color& newColor, const float changePct) +{ + newColor.red = LerpUint8_t(oldColor.red, newColor.red, changePct); + newColor.green = LerpUint8_t(oldColor.green, newColor.green, changePct); + newColor.blue = LerpUint8_t(oldColor.blue, newColor.blue, changePct); +} + +void LerpDirectional(RE::BGSDirectionalAmbientLightingColors::Directional& oldColor, RE::BGSDirectionalAmbientLightingColors::Directional& newColor, const float changePct) +{ + LerpColor(oldColor.x.max, newColor.x.max, changePct); + LerpColor(oldColor.x.min, newColor.x.min, changePct); + LerpColor(oldColor.y.max, newColor.y.max, changePct); + LerpColor(oldColor.y.min, newColor.y.min, changePct); + LerpColor(oldColor.z.max, newColor.z.max, changePct); + LerpColor(oldColor.z.min, newColor.z.min, changePct); +} + +void WeatherEditor::DrawSettings() +{ + ImGui::TextWrapped("Weather Editor provides controls for saving settings and opening the weather editor window."); + ImGui::Spacing(); + + if (ImGui::BeginTable("##WeatherEditorButtons", 2, ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextColumn(); + if (ImGui::Button("Open Editor", { -1, 0 })) { + EditorWindow::GetSingleton()->open = true; + } + + ImGui::TableNextColumn(); + if (ImGui::Button("Save Settings", { -1, 0 })) { + State::GetSingleton()->Save(); + } + + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + DrawWeatherStatusPanel(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + DrawQuickWeatherSpawner(); +} + +void WeatherEditor::Bind() +{ + if (loaded) { + auto& context = globals::d3d::context; + + // Set PS shader resource + { + ID3D11ShaderResourceView* srv = diffuseIBLTexture->srv.get(); + context->PSSetShaderResources(80, 1, &srv); + } + } +} + +void WeatherEditor::Prepass() +{ + auto& context = globals::d3d::context; + + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); + auto& reflections = renderer->GetRendererData().cubemapRenderTargets[RE::RENDER_TARGET_CUBEMAP::kREFLECTIONS]; + + std::array srvs = { reflections.SRV }; + std::array uavs = { diffuseIBLTexture->uav.get() }; + std::array samplers = { Deferred::GetSingleton()->linearSampler }; + + // Unset PS shader resource + { + ID3D11ShaderResourceView* srv = nullptr; + context->PSSetShaderResources(80, 1, &srv); + } + + // IBL + { + samplers[0] = Deferred::GetSingleton()->linearSampler; + + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(GetDiffuseIBLCS(), nullptr, 0); + context->Dispatch(1, 1, 1); + } + + // Reset + { + srvs.fill(nullptr); + uavs.fill(nullptr); + samplers.fill(nullptr); + + context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); + context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); + context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); + context->CSSetShader(nullptr, nullptr, 0); + } + + // Set PS shader resource + { + ID3D11ShaderResourceView* srv = diffuseIBLTexture->srv.get(); + context->PSSetShaderResources(100, 1, &srv); + } +} + +void WeatherEditor::SetupResources() +{ + GetDiffuseIBLCS(); + + { + D3D11_TEXTURE2D_DESC texDesc{ + .Width = 3, + .Height = 1, + .MipLevels = 1, + .ArraySize = 1, + .Format = DXGI_FORMAT_R16G16B16A16_FLOAT, + .SampleDesc = { 1, 0 }, + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS, + .CPUAccessFlags = 0, + .MiscFlags = 0 + }; + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = texDesc.MipLevels } + }; + D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { + .Format = texDesc.Format, + .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, + .Texture2D = { .MipSlice = 0 } + }; + + diffuseIBLTexture = new Texture2D(texDesc); + diffuseIBLTexture->CreateSRV(srvDesc); + diffuseIBLTexture->CreateUAV(uavDesc); + } +} + +void WeatherEditor::ClearShaderCache() +{ + if (diffuseIBLCS) + diffuseIBLCS->Release(); + diffuseIBLCS = nullptr; +} + +ID3D11ComputeShader* WeatherEditor::GetDiffuseIBLCS() +{ + if (!diffuseIBLCS) + diffuseIBLCS = static_cast(Util::CompileShader(L"Data\\Shaders\\Weather\\DiffuseIBLCS.hlsl", {}, "cs_5_0")); + return diffuseIBLCS; +} + +void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeather, float currentWeatherPct) +{ + //// Precipitation + newWeather->data.precipitationBeginFadeIn = LerpInt8_t(oldWeather->data.precipitationBeginFadeIn, newWeather->data.precipitationBeginFadeIn, currentWeatherPct); + newWeather->data.precipitationEndFadeOut = LerpInt8_t(oldWeather->data.precipitationEndFadeOut, newWeather->data.precipitationEndFadeOut, currentWeatherPct); + + //// Sun + newWeather->data.sunDamage = LerpInt8_t(oldWeather->data.sunDamage, newWeather->data.sunDamage, currentWeatherPct); + newWeather->data.sunGlare = LerpInt8_t(oldWeather->data.sunGlare, newWeather->data.sunGlare, currentWeatherPct); + + //// Lightning + newWeather->data.thunderLightningBeginFadeIn = LerpInt8_t(oldWeather->data.thunderLightningBeginFadeIn, newWeather->data.thunderLightningBeginFadeIn, currentWeatherPct); + newWeather->data.thunderLightningEndFadeOut = LerpInt8_t(oldWeather->data.thunderLightningEndFadeOut, newWeather->data.thunderLightningEndFadeOut, currentWeatherPct); + newWeather->data.thunderLightningFrequency = LerpInt8_t(oldWeather->data.thunderLightningFrequency, newWeather->data.thunderLightningFrequency, currentWeatherPct); + LerpColor(oldWeather->data.lightningColor, newWeather->data.lightningColor, currentWeatherPct); + + //// Trans delta + newWeather->data.transDelta = LerpInt8_t(oldWeather->data.transDelta, newWeather->data.transDelta, currentWeatherPct); + + //// Visual Effects + newWeather->data.visualEffectBegin = LerpInt8_t(oldWeather->data.visualEffectBegin, newWeather->data.visualEffectBegin, currentWeatherPct); + newWeather->data.visualEffectEnd = LerpInt8_t(oldWeather->data.visualEffectEnd, newWeather->data.visualEffectEnd, currentWeatherPct); + + //// Wind + newWeather->data.windDirection = LerpInt8_t(oldWeather->data.windDirection, newWeather->data.windDirection, currentWeatherPct); + newWeather->data.windDirectionRange = LerpInt8_t(oldWeather->data.windDirectionRange, newWeather->data.windDirectionRange, currentWeatherPct); + newWeather->data.windSpeed = LerpUint8_t(oldWeather->data.windSpeed, newWeather->data.windSpeed, currentWeatherPct); + + //// Fog + newWeather->fogData.dayFar = std::lerp(oldWeather->fogData.dayFar, newWeather->fogData.dayFar, currentWeatherPct); + newWeather->fogData.dayMax = std::lerp(oldWeather->fogData.dayMax, newWeather->fogData.dayMax, currentWeatherPct); + newWeather->fogData.dayNear = std::lerp(oldWeather->fogData.dayNear, newWeather->fogData.dayNear, currentWeatherPct); + newWeather->fogData.dayPower = std::lerp(oldWeather->fogData.dayPower, newWeather->fogData.dayPower, currentWeatherPct); + + newWeather->fogData.nightFar = std::lerp(oldWeather->fogData.nightFar, newWeather->fogData.nightFar, currentWeatherPct); + newWeather->fogData.nightMax = std::lerp(oldWeather->fogData.nightMax, newWeather->fogData.nightMax, currentWeatherPct); + newWeather->fogData.nightNear = std::lerp(oldWeather->fogData.nightNear, newWeather->fogData.nightNear, currentWeatherPct); + newWeather->fogData.nightPower = std::lerp(oldWeather->fogData.nightPower, newWeather->fogData.nightPower, currentWeatherPct); + + //// Weather colors + for (size_t i = 0; i < RE::TESWeather::ColorTypes::kTotal; i++) { + for (size_t j = 0; j < RE::TESWeather::ColorTime::kTotal; j++) { + LerpColor(oldWeather->colorData[i][j], newWeather->colorData[i][j], currentWeatherPct); + } + } + + //// DALC + for (size_t i = 0; i < RE::TESWeather::ColorTime::kTotal; i++) { + auto& newDALC = newWeather->directionalAmbientLightingColors[i]; + auto& oldDALC = oldWeather->directionalAmbientLightingColors[i]; + + LerpColor(oldDALC.specular, newDALC.specular, currentWeatherPct); + newWeather->directionalAmbientLightingColors[i].fresnelPower = std::lerp(oldDALC.fresnelPower, newDALC.fresnelPower, currentWeatherPct); + LerpDirectional(oldDALC.directional, newDALC.directional, currentWeatherPct); + } + + //// Clouds + for (size_t i = 0; i < RE::TESWeather::kTotalLayers; i++) { + for (size_t j = 0; j < RE::TESWeather::ColorTime::kTotal; j++) { + LerpColor(oldWeather->cloudColorData[i][j], newWeather->cloudColorData[i][j], currentWeatherPct); + newWeather->cloudAlpha[i][j] = std::lerp(oldWeather->cloudAlpha[i][j], newWeather->cloudAlpha[i][j], currentWeatherPct); + } + + newWeather->cloudLayerSpeedY[i] = LerpInt8_t(oldWeather->cloudLayerSpeedY[i], newWeather->cloudLayerSpeedY[i], currentWeatherPct); + newWeather->cloudLayerSpeedX[i] = LerpInt8_t(oldWeather->cloudLayerSpeedX[i], newWeather->cloudLayerSpeedX[i], currentWeatherPct); + } +} + +void WeatherEditor::DrawWeatherStatusPanel() +{ + if (ImGui::CollapsingHeader("Current Weather Status", ImGuiTreeNodeFlags_DefaultOpen)) { + auto weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + if (currentWeathers.currentWeather) { + ImGui::Text("Current Weather: %s", + currentWeathers.currentWeather->GetFormEditorID() ? + currentWeathers.currentWeather->GetFormEditorID() : + std::format("{:08X}", currentWeathers.currentWeather->GetFormID()).c_str()); + + if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { + ImGui::Text("Transitioning From: %s", + currentWeathers.lastWeather->GetFormEditorID() ? + currentWeathers.lastWeather->GetFormEditorID() : + std::format("{:08X}", currentWeathers.lastWeather->GetFormID()).c_str()); + + ImGui::ProgressBar(currentWeathers.lerpFactor, ImVec2(-1, 0), + std::format("Transition: {:.1f}%%", currentWeathers.lerpFactor * 100.0f).c_str()); + } else { + ImGui::Text("Transition: Complete (100%%)"); + } + + // Show if weather has custom settings + if (weatherManager->HasWeatherSettings(currentWeathers.currentWeather)) { + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has Custom Settings"); + } else { + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using Default Settings"); + } + } else { + ImGui::TextColored({ 1.0f, 0.5f, 0.0f, 1.0f }, "No Active Weather"); + } + } +} + +void WeatherEditor::DrawQuickWeatherSpawner() +{ + if (ImGui::CollapsingHeader("Quick Weather Spawner")) { + ImGui::TextWrapped("Quickly test different weather conditions. Note: Weather changes may take time to transition."); + ImGui::Spacing(); + + static char weatherFilter[128] = ""; + ImGui::InputTextWithHint("##WeatherFilter", "Search weathers...", weatherFilter, sizeof(weatherFilter)); + + if (ImGui::BeginChild("WeatherList", ImVec2(0, 200), true)) { + auto dataHandler = RE::TESDataHandler::GetSingleton(); + if (dataHandler) { + auto& weatherArray = dataHandler->GetFormArray(); + + for (auto* weather : weatherArray) { + if (!weather) + continue; + + const char* editorID = weather->GetFormEditorID(); + std::string displayName = editorID ? editorID : std::format("{:08X}", weather->GetFormID()); + + // Filter + if (weatherFilter[0] != '\0' && + displayName.find(weatherFilter) == std::string::npos) { + continue; + } + + if (ImGui::Selectable(displayName.c_str())) { + // Force weather change + auto sky = RE::Sky::GetSingleton(); + if (sky) { + sky->ForceWeather(weather, false); + } + } + + // Show if this weather has custom settings + if (WeatherManager::GetSingleton()->HasWeatherSettings(weather)) { + ImGui::SameLine(); + ImGui::TextColored({ 0.0f, 0.8f, 0.0f, 1.0f }, "[Modified]"); + } + } + } + } + ImGui::EndChild(); + + if (ImGui::Button("Reset to Natural Weather", ImVec2(-1, 0))) { + auto sky = RE::Sky::GetSingleton(); + if (sky) { + sky->ReleaseWeatherOverride(); + } + } + } +} diff --git a/src/Features/WeatherEditor.h b/src/Features/WeatherEditor.h new file mode 100644 index 0000000000..5b7704ec33 --- /dev/null +++ b/src/Features/WeatherEditor.h @@ -0,0 +1,47 @@ +#pragma once + +#include "Buffer.h" +#include "Feature.h" +#include "State.h" + +struct WeatherEditor : Feature +{ +public: + static WeatherEditor* GetSingleton() + { + static WeatherEditor singleton; + return &singleton; + } + + virtual inline std::string GetName() override { return "Weather Editor"; } + virtual inline std::string GetShortName() override { return "WeatherEditor"; } + virtual inline std::string_view GetShaderDefineName() override { return "WEATHER"; } + virtual inline std::string_view GetCategory() const override { return "Debug"; } + virtual inline std::pair> GetFeatureSummary() override + { + return { + "Development tool for editing weather settings and testing IBL.", + { "Provides weather editing functionality", + "Includes save/load settings controls", + "Real-time weather transition monitoring", + "Debug feature for development" } + }; + } + + Texture2D* diffuseIBLTexture = nullptr; + ID3D11ComputeShader* diffuseIBLCS = nullptr; + + void Bind(); + + virtual void DrawSettings() override; + virtual void Prepass() override; + virtual void SetupResources() override; + virtual void ClearShaderCache() override; + + ID3D11ComputeShader* GetDiffuseIBLCS(); + void LerpWeather(RE::TESWeather*, RE::TESWeather*, float); + +private: + void DrawWeatherStatusPanel(); + void DrawQuickWeatherSpawner(); +}; diff --git a/src/Globals.cpp b/src/Globals.cpp index 7d6995ad45..1c9ccac247 100644 --- a/src/Globals.cpp +++ b/src/Globals.cpp @@ -28,6 +28,7 @@ #include "Features/VR.h" #include "Features/VolumetricLighting.h" #include "Features/WaterEffects.h" +#include "Features/WeatherEditor.h" #include "Features/WeatherPicker.h" #include "Features/WetnessEffects.h" #include "Menu.h" @@ -76,6 +77,7 @@ namespace globals ExtendedTranslucency extendedTranslucency{}; Upscaling upscaling{}; RenderDoc renderDoc{}; + WeatherEditor weatherEditor{}; namespace llf { @@ -88,7 +90,9 @@ namespace globals RE::BSGraphics::State* graphicsState = nullptr; RE::BSGraphics::Renderer* renderer = nullptr; RE::BSShaderManager::State* smState = nullptr; + RE::TES* tes = nullptr; bool isVR = false; + RE::MemoryManager* memoryManager = nullptr; RE::INISettingCollection* iniSettingCollection = nullptr; RE::INIPrefSettingCollection* iniPrefSettingCollection = nullptr; RE::GameSettingCollection* gameSettingCollection = nullptr; diff --git a/src/Globals.h b/src/Globals.h index cf52ded482..bd4db516aa 100644 --- a/src/Globals.h +++ b/src/Globals.h @@ -28,6 +28,7 @@ struct PerformanceOverlay; struct WetnessEffects; struct ExtendedTranslucency; struct Upscaling; +struct WeatherEditor; class State; class Deferred; @@ -80,6 +81,7 @@ namespace globals extern ExtendedTranslucency extendedTranslucency; extern Upscaling upscaling; extern RenderDoc renderDoc; + extern WeatherEditor weatherEditor; namespace llf { @@ -199,7 +201,9 @@ namespace globals extern RE::BSGraphics::State* graphicsState; extern RE::BSGraphics::Renderer* renderer; extern RE::BSShaderManager::State* smState; + extern RE::TES* tes; extern bool isVR; + extern RE::MemoryManager* memoryManager; extern RE::INISettingCollection* iniSettingCollection; extern RE::INIPrefSettingCollection* iniPrefSettingCollection; extern RE::GameSettingCollection* gameSettingCollection; diff --git a/src/Menu.cpp b/src/Menu.cpp index 6cbbd0bbda..f7862d0b9c 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -42,6 +42,7 @@ #include "Features/PerformanceOverlay/ABTesting/ABTesting.h" #include "Features/VR.h" #include "Features/WeatherPicker.h" +#include "WeatherEditor/EditorWindow.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( Menu::ThemeSettings::PaletteColors, @@ -413,6 +414,11 @@ void Menu::DrawSettings() ImGui::SetNextWindowSize(Util::GetNativeViewportSizeScaled(0.8f), ImGuiCond_FirstUseEver); auto title = std::format("Community Shaders {}", Util::GetFormattedVersion(Plugin::VERSION)); + if (EditorWindow::GetSingleton()->open) { + EditorWindow::GetSingleton()->Draw(); + return; + } + // Determine window flags based on docking state ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar; // Check if this will be docked (we need to peek at the docking state) diff --git a/src/State.cpp b/src/State.cpp index 3846ab0046..dfc514af1c 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -16,6 +16,7 @@ #include "ShaderCache.h" #include "TruePBR.h" #include "Utils/FileSystem.h" +#include "WeatherManager.h" void State::Draw() { @@ -27,6 +28,9 @@ void State::Draw() auto truePBR = globals::truePBR; auto context = globals::d3d::context; + // Update weather-based feature settings + WeatherManager::GetSingleton()->UpdateFeatures(); + if (shaderCache->IsEnabled()) { if (terrainBlending.loaded) terrainBlending.TerrainShaderHacks(); @@ -153,6 +157,9 @@ void State::Setup() if (feature->loaded) feature->SetupResources(); globals::deferred->SetupResources(); + + // Load per-weather settings after features are setup + WeatherManager::GetSingleton()->LoadPerWeatherSettingsFromDisk(); } static std::string GetConfigPath(State::ConfigMode a_configMode) diff --git a/src/Util.h b/src/Util.h index 55ed6b583d..e8258f4de9 100644 --- a/src/Util.h +++ b/src/Util.h @@ -8,3 +8,4 @@ #include "Utils/Serialize.h" #include "Utils/UI.h" #include "Utils/WinApi.h" +#include "Utils/Form.h" diff --git a/src/Utils/Form.cpp b/src/Utils/Form.cpp new file mode 100644 index 0000000000..427c71eec1 --- /dev/null +++ b/src/Utils/Form.cpp @@ -0,0 +1,22 @@ +#include "Form.h" + +auto Util::GetFormFromIdentifier(const std::string& a_identifier) -> RE::TESForm* +{ + std::istringstream ss{ a_identifier }; + std::string plugin, id; + + std::getline(ss, plugin, '|'); + std::getline(ss, id); + RE::FormID relativeID; + std::istringstream{ id } >> std::hex >> relativeID; + const auto dataHandler = RE::TESDataHandler::GetSingleton(); + return dataHandler ? dataHandler->LookupForm(relativeID, plugin) : nullptr; +} + +auto Util::GetIdentifierFromForm(const RE::TESForm* a_form) -> std::string +{ + if (auto file = a_form->GetFile()) { + return std::format("{:X}|{}", a_form->GetLocalFormID(), file->GetFilename()); + } + return std::format("{:X}|Generated", a_form->GetLocalFormID()); +} diff --git a/src/Utils/Form.h b/src/Utils/Form.h new file mode 100644 index 0000000000..85215ce375 --- /dev/null +++ b/src/Utils/Form.h @@ -0,0 +1,7 @@ +#pragma once + +namespace Util +{ + auto GetFormFromIdentifier(const std::string& a_identifier) -> RE::TESForm*; + auto GetIdentifierFromForm(const RE::TESForm* a_form) -> std::string; +} diff --git a/src/Utils/Serialize.cpp b/src/Utils/Serialize.cpp index 20d8a0da8d..20f769d6b4 100644 --- a/src/Utils/Serialize.cpp +++ b/src/Utils/Serialize.cpp @@ -74,4 +74,31 @@ namespace nlohmann result[2] = section[2]; } } + + void to_json(json& j, const RE::TESWeather::FogData& fog) + { + j = { + fog.dayNear, + fog.dayFar, + fog.nightNear, + fog.nightFar, + fog.dayPower, + fog.nightPower, + fog.dayMax, + fog.nightMax + }; + } + + void from_json(const json& j, RE::TESWeather::FogData& fog) + { + std::array temp = j; + fog = { j[0], + j[1], + j[2], + j[3], + j[4], + j[5], + j[6], + j[7] }; + } } \ No newline at end of file diff --git a/src/Utils/Serialize.h b/src/Utils/Serialize.h index f5b91add59..656b20a29c 100644 --- a/src/Utils/Serialize.h +++ b/src/Utils/Serialize.h @@ -15,4 +15,7 @@ namespace nlohmann void to_json(json&, const RE::NiColor&); void from_json(const json&, RE::NiColor&); + + void to_json(json& j, const RE::TESWeather::FogData& fog); + void from_json(const json& j, RE::TESWeather::FogData& fog); }; \ No newline at end of file diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp new file mode 100644 index 0000000000..65c69951d2 --- /dev/null +++ b/src/WeatherEditor/EditorWindow.cpp @@ -0,0 +1,639 @@ +#include "EditorWindow.h" + +#include "State.h" +#include "features/Weather.h" +#include "imgui_internal.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(EditorWindow::Settings, recordMarkers, markedRecords) + +bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring) +{ + const auto it = std::ranges::search(a_string, a_substring, [](const char a_a, const char a_b) { + return std::tolower(a_a) == std::tolower(a_b); + }).begin(); + return it != a_string.end(); +} + +void TextUnformattedDisabled(const char* a_text, const char* a_textEnd = nullptr) +{ + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextUnformatted(a_text, a_textEnd); + ImGui::PopStyleColor(); +} + +void AddTooltip(const char* a_desc, ImGuiHoveredFlags a_flags = ImGuiHoveredFlags_DelayNormal) +{ + if (ImGui::IsItemHovered(a_flags)) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 8, 8 }); + if (ImGui::BeginTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 50.0f); + ImGui::TextUnformatted(a_desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + ImGui::PopStyleVar(); + } +} + +inline void HelpMarker(const char* a_desc) +{ + ImGui::AlignTextToFramePadding(); + TextUnformattedDisabled("(?)"); + AddTooltip(a_desc, ImGuiHoveredFlags_DelayShort); +} + +void EditorWindow::ShowObjectsWindow() +{ + ImGui::Begin("Object List"); + + // Static variable to track the selected category + static std::string selectedCategory = "Lighting Template"; + + // Static variable for filtering objects + static char filterBuffer[256] = ""; + + // Create a table with two columns + if (ImGui::BeginTable("ObjectTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner | ImGuiTableFlags_NoHostExtendX)) { + // Set up column widths + ImGui::TableSetupColumn("Categories", ImGuiTableColumnFlags_WidthStretch, 0.3f); // 30% width + ImGui::TableSetupColumn("Objects", ImGuiTableColumnFlags_WidthStretch, 0.7f); // 70% width + + ImGui::TableNextRow(); + + // Left column: Categories + ImGui::TableSetColumnIndex(0); + + // Begin a table for the categories list + if (ImGui::BeginTable("CategoriesTable", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Categories"); // Label for the table + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + // List of categories + const char* categories[] = { "Lighting Template", "Weather", "WorldSpace" }; + for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { + // Highlight the selected category + if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { + selectedCategory = categories[i]; // Update selected category + } + } + + ImGui::EndTable(); + } + + // Right column: Objects + ImGui::TableSetColumnIndex(1); + + ImGui::InputTextWithHint("##ObjectFilter", "Filter...", filterBuffer, sizeof(filterBuffer)); + + ImGui::SameLine(); + HelpMarker("Type a part of an object name to filter the list."); + + // Create a table for the right column with "Name" and "ID" headers + if (ImGui::BeginTable("DetailsTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch); // Added File column + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch); + + ImGui::TableHeadersRow(); + + // Display objects based on the selected category + auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + lightingTemplateWidgets; + + // Filtered display of widgets + for (int i = 0; i < widgets.size(); ++i) { + if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) + continue; // Skip widgets that don't match the filter + + auto editorLabel = widgets[i]->GetEditorID(); + auto markedRecord = settings.markedRecords.find(editorLabel); + ImGui::TableNextRow(); + + // Set background colour + if (markedRecord != settings.markedRecords.end()) { + auto& color = settings.recordMarkers[markedRecord->second]; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); + } else { + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGuiCol_TableRowBg); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGuiCol_TableRowBgAlt); + } + + ImGui::TableSetColumnIndex(0); + + // Editor ID column + if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + widgets[i]->SetOpen(true); + } + } + + // Opens a context menu on right click to mark records by color + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; + + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[editorLabel] = recordMarker.first; + Save(); + } + } + + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(editorLabel); + Save(); + } + + ImGui::EndPopup(); + } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text(widgets[i]->GetFormID().c_str()); + + // File column + ImGui::TableNextColumn(); + ImGui::Text(widgets[i]->GetFilename().c_str()); + + // Status column + ImGui::TableNextColumn(); + + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text(markedRecord->second.c_str()); + } + } + + ImGui::EndTable(); + } + + ImGui::EndTable(); + } + + // End the window + ImGui::End(); +} + +void EditorWindow::ShowViewportWindow() +{ + ImGui::Begin("Viewport"); + + // Top bar + auto calendar = RE::Calendar::GetSingleton(); + if (calendar) + ImGui::SliderFloat("##ViewportSlider", &calendar->gameHour->value, 0.0f, 23.99f, "Time: %.2f"); + + // The size of the image in ImGui // Get the available space in the current window + ImVec2 availableSpace = ImGui::GetContentRegionAvail(); + + // Calculate aspect ratio of the image + float aspectRatio = ImGui::GetIO().DisplaySize.x / ImGui::GetIO().DisplaySize.y; + + // Determine the size to fit while preserving the aspect ratio + ImVec2 imageSize; + if (availableSpace.x / availableSpace.y < aspectRatio) { + // Fit width + imageSize.x = availableSpace.x; + imageSize.y = availableSpace.x / aspectRatio; + } else { + // Fit height + imageSize.y = availableSpace.y; + imageSize.x = availableSpace.y * aspectRatio; + } + + ImGui::Image((void*)tempTexture->srv.get(), imageSize); + + ImGui::End(); +} + +void EditorWindow::ShowWidgetWindow() +{ + for (int i = 0; i < (int)weatherWidgets.size(); i++) { + auto widget = weatherWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } + + for (int i = 0; i < (int)worldSpaceWidgets.size(); i++) { + auto widget = worldSpaceWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } + + for (int i = 0; i < (int)lightingTemplateWidgets.size(); i++) { + auto widget = lightingTemplateWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } +} + +void EditorWindow::RenderUI() +{ + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); + auto& framebuffer = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; + auto& context = globals::d3d::context; + + context->ClearRenderTargetView(framebuffer.RTV, (float*)&ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); + + // Check for Escape key to close editor + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + open = false; + } + + if (ImGui::BeginMainMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Save All Open Widgets", "Ctrl+S")) { + SaveAll(); + } + ImGui::Separator(); + if (ImGui::MenuItem("Close All Weather Widgets")) { + for (auto* widget : weatherWidgets) widget->SetOpen(false); + } + if (ImGui::MenuItem("Close All WorldSpace Widgets")) { + for (auto* widget : worldSpaceWidgets) widget->SetOpen(false); + } + if (ImGui::MenuItem("Close All Lighting Widgets")) { + for (auto* widget : lightingTemplateWidgets) widget->SetOpen(false); + } + ImGui::Separator(); + if (ImGui::MenuItem("Editor Settings")) { + showSettingsWindow = !showSettingsWindow; + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Window")) { + ImGui::Text("Open Widgets:"); + ImGui::Separator(); + + int openCount = 0; + for (auto* widget : weatherWidgets) { + if (widget->IsOpen()) { + openCount++; + if (ImGui::MenuItem(std::format("Weather: {}", widget->GetEditorID()).c_str())) { + // Focus window (ImGui will bring to front when clicked) + } + } + } + for (auto* widget : worldSpaceWidgets) { + if (widget->IsOpen()) { + openCount++; + if (ImGui::MenuItem(std::format("WorldSpace: {}", widget->GetEditorID()).c_str())) { + // Focus window + } + } + } + for (auto* widget : lightingTemplateWidgets) { + if (widget->IsOpen()) { + openCount++; + if (ImGui::MenuItem(std::format("Lighting: {}", widget->GetEditorID()).c_str())) { + // Focus window + } + } + } + + if (openCount == 0) { + ImGui::TextDisabled("No widgets open"); + } + + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Help")) { + ImGui::Text("Weather Editor"); + ImGui::Separator(); + ImGui::BulletText("Double-click objects to edit"); + ImGui::BulletText("Right-click to mark status"); + ImGui::BulletText("Auto-Apply updates game live"); + ImGui::BulletText("Lock weather to prevent changes"); + ImGui::BulletText("Changes save to JSON files"); + ImGui::Separator(); + ImGui::Text("Total Objects:"); + ImGui::BulletText("Weathers: %d", (int)weatherWidgets.size()); + ImGui::BulletText("WorldSpaces: %d", (int)worldSpaceWidgets.size()); + ImGui::BulletText("Lighting: %d", (int)lightingTemplateWidgets.size()); + ImGui::EndMenu(); + } + + // Weather lock indicator + if (weatherLockActive && lockedWeather) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); + const char* weatherName = lockedWeather->GetFormEditorID(); + ImGui::Text(" [LOCKED: %s]", weatherName ? weatherName : "Unknown"); + ImGui::PopStyleColor(); + } + + // Time pause indicator + if (timePaused) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); + ImGui::Text(" [TIME PAUSED]"); + ImGui::PopStyleColor(); + } + + // Close button on the right side + float menuBarHeight = ImGui::GetFrameHeight(); + ImGui::SameLine(ImGui::GetWindowWidth() - menuBarHeight); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button("X", ImVec2(menuBarHeight, menuBarHeight))) { + open = false; + } + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Close Weather Editor (Esc)"); + } + + ImGui::EndMainMenuBar(); + } + + auto width = ImGui::GetIO().DisplaySize.x; + auto height = ImGui::GetIO().DisplaySize.y; + auto viewportWidth = width * 0.5f; // Make the viewport take up 50% of the width + auto sideWidth = (width - viewportWidth) / 2.0f; // Divide the remaining width equally between the side windows + ImGui::SetNextWindowSize(ImVec2(sideWidth, ImGui::GetIO().DisplaySize.y * 0.75f), ImGuiCond_FirstUseEver); + ShowObjectsWindow(); + + ImGui::SetNextWindowSize(ImVec2(viewportWidth, ImGui::GetIO().DisplaySize.y * 0.5f), ImGuiCond_FirstUseEver); + ShowViewportWindow(); + + auto settingsWindowHeight = height * 0.25f; + auto settingsWindowWidth = width * 0.25f; + ImGui::SetNextWindowSizeConstraints(ImVec2(settingsWindowWidth, settingsWindowHeight), ImVec2(FLT_MAX, FLT_MAX)); + ImGui::SetNextWindowPos({ (width / 2.0f) - (settingsWindowWidth / 2.0f), (height / 2.0f) - (settingsWindowHeight / 2.0f) }, ImGuiCond_Appearing); + if (showSettingsWindow) { + ShowSettingsWindow(); + } + + ShowWidgetWindow(); +} + +void EditorWindow::SetupResources() +{ + auto dataHandler = RE::TESDataHandler::GetSingleton(); + auto& weatherArray = dataHandler->GetFormArray(); + + Load(); + + for (auto weather : weatherArray) { + auto widget = new WeatherWidget(weather); + widget->Load(); + weatherWidgets.push_back(widget); + } + + auto& worldSpaceArray = dataHandler->GetFormArray(); + + for (auto worldSpace : worldSpaceArray) { + auto widget = new WorldSpaceWidget(worldSpace); + widget->Load(); + worldSpaceWidgets.push_back(widget); + } + + auto& lightingTemplateArray = dataHandler->GetFormArray(); + + for (auto lightingTemplate : lightingTemplateArray) { + auto widget = new LightingTemplateWidget(lightingTemplate); + widget->Load(); + lightingTemplateWidgets.push_back(widget); + } +} + +void EditorWindow::Draw() +{ + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); + auto& framebuffer = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; + + ID3D11Resource* resource; + framebuffer.SRV->GetResource(&resource); + + if (!tempTexture) { + D3D11_TEXTURE2D_DESC texDesc{}; + ((ID3D11Texture2D*)resource)->GetDesc(&texDesc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc{}; + framebuffer.SRV->GetDesc(&srvDesc); + + tempTexture = new Texture2D(texDesc); + tempTexture->CreateSRV(srvDesc); + } + + auto& context = globals::d3d::context; + + context->CopyResource(tempTexture->resource.get(), resource); + + RenderUI(); +} + +void EditorWindow::SaveAll() +{ + for (auto weather : weatherWidgets) { + if (weather->IsOpen()) + weather->Save(); + } + + for (auto worldspace : worldSpaceWidgets) { + if (worldspace->IsOpen()) + worldspace->Save(); + } + + for (auto lightingTemplate : lightingTemplateWidgets) { + if (lightingTemplate->IsOpen()) + lightingTemplate->Save(); + } + + Save(); +} + +void EditorWindow::SaveSettings() +{ + j = settings; +} + +void EditorWindow::LoadSettings() +{ + if (!j.empty()) + settings = j; +} + +void EditorWindow::ShowSettingsWindow() +{ + ImGui::Begin("Settings"); + + // Static variable to track the selected category + static std::string selectedOption = "Preferences"; + + // Create a table with two columns + if (ImGui::BeginTable("SettingsTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner | ImGuiTableFlags_NoHostExtendX)) { + // Set up column widths + ImGui::TableSetupColumn("Options", ImGuiTableColumnFlags_WidthStretch, 0.3f); // 30% width + ImGui::TableSetupColumn("##Settings", ImGuiTableColumnFlags_WidthStretch, 0.7f); // 70% width + + ImGui::TableNextRow(); + + // Left column: Options + ImGui::TableSetColumnIndex(0); + // List of options + const char* options[] = { "Preferences" }; + for (int i = 0; i < IM_ARRAYSIZE(options); ++i) { + if (ImGui::Selectable(options[i], selectedOption == options[i])) { + selectedOption = options[i]; // Update selected option + } + } + + // Right column: Option settings + ImGui::TableSetColumnIndex(1); + + // Create a table for the right column. + if (selectedOption == "Preferences") { + if (ImGui::BeginTable("PreferencesTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Colour", ImGuiTableColumnFlags_WidthStretch); + + auto& recordMarkers = settings.recordMarkers; + + for (auto& recordMarker : recordMarkers) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text(recordMarker.first.c_str()); + + ImGui::TableSetColumnIndex(1); + if (ImGui::ColorEdit4(std::format("Color##{}", recordMarker.first).c_str(), (float*)&recordMarker.second)) { + Save(); + }; + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + if (recordMarkers.size() < maxRecordMarkers && ImGui::Selectable("Add new marker")) { + recordMarkers.insert({ std::string("New marker##new{}", recordMarkers.size()), { 0, 0, 0, 255 } }); + Save(); + } + + ImGui::EndTable(); + } + } + ImGui::EndTable(); + } + + ImGui::End(); +} + +void EditorWindow::Save() +{ + SaveSettings(); + const std::string filePath = Util::PathHelpers::GetCommunityShaderPath().string(); + const std::string file = std::format("{}\\{}.json", filePath, settingsFilename); + + std::ofstream settingsFile(file); + + if (!settingsFile.good() || !settingsFile.is_open()) { + logger::warn("Failed to open settings file: {}", file); + return; + } + + if (settingsFile.fail()) { + logger::warn("Unable to create settings file: {}", file); + settingsFile.close(); + return; + } + + logger::info("Saving settings file: {}", file); + + try { + settingsFile << j.dump(1); + + settingsFile.close(); + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing settings for settings file ({}) : {}\n", filePath, e.what()); + settingsFile.close(); + } +} + +void EditorWindow::Load() +{ + std::string filePath = std::format("{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), settingsFilename); + + std::ifstream settingsFile(filePath); + + if (!std::filesystem::exists(filePath)) { + // Does not have any settings so just return. + return; + } + + if (!settingsFile.good() || !settingsFile.is_open()) { + logger::warn("Failed to load settings file: {}", filePath); + return; + } + + try { + j << settingsFile; + settingsFile.close(); + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing settings for file ({}) : {}\n", filePath, e.what()); + settingsFile.close(); + } + LoadSettings(); +} + +void EditorWindow::LockWeather(RE::TESWeather* weather) +{ + if (!weather) return; + + auto sky = RE::Sky::GetSingleton(); + if (!sky) return; + + // Force the weather to be active + sky->ForceWeather(weather, false); + + lockedWeather = weather; + weatherLockActive = true; + + logger::info("Weather locked: {}", weather->GetFormEditorID() ? weather->GetFormEditorID() : "Unknown"); +} + +void EditorWindow::UnlockWeather() +{ + if (!weatherLockActive) return; + + auto sky = RE::Sky::GetSingleton(); + if (sky) { + // Release weather override to allow natural progression + sky->ReleaseWeatherOverride(); + } + + logger::info("Weather unlocked: {}", lockedWeather && lockedWeather->GetFormEditorID() ? lockedWeather->GetFormEditorID() : "Unknown"); + + lockedWeather = nullptr; + weatherLockActive = false; +} + +void EditorWindow::PauseTime() +{ + if (timePaused) return; + + auto calendar = RE::Calendar::GetSingleton(); + if (calendar && calendar->timeScale) { + savedTimeScale = calendar->timeScale->value; + calendar->timeScale->value = 0.0f; + timePaused = true; + logger::info("Time paused (saved timescale: {})", savedTimeScale); + } +} + +void EditorWindow::ResumeTime() +{ + if (!timePaused) return; + + auto calendar = RE::Calendar::GetSingleton(); + if (calendar && calendar->timeScale) { + calendar->timeScale->value = savedTimeScale; + timePaused = false; + logger::info("Time resumed (timescale: {})", savedTimeScale); + } +} diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h new file mode 100644 index 0000000000..f4e2022fb6 --- /dev/null +++ b/src/WeatherEditor/EditorWindow.h @@ -0,0 +1,80 @@ +#pragma once + +#include "Buffer.h" + +#include "Weather/LightingTemplateWidget.h" +#include "Weather/WeatherWidget.h" +#include "Weather/WorldSpaceWidget.h" +#include "WeatherUtils.h" +#include "Widget.h" + +class EditorWindow +{ +public: + static EditorWindow* GetSingleton() + { + static EditorWindow singleton; + return &singleton; + } + + bool open = false; + const static int maxRecordMarkers = 10; + + Texture2D* tempTexture; + + std::vector weatherWidgets; + std::vector worldSpaceWidgets; + std::vector lightingTemplateWidgets; + + // Weather locking for editing + RE::TESWeather* lockedWeather = nullptr; + bool weatherLockActive = false; + + // Time pause for editing + bool timePaused = false; + float savedTimeScale = 1.0f; + + void ShowObjectsWindow(); + + void ShowViewportWindow(); + + void ShowWidgetWindow(); + + void RenderUI(); + + void SetupResources(); + + void Draw(); + + void LockWeather(RE::TESWeather* weather); + void UnlockWeather(); + bool IsWeatherLocked() const { return weatherLockActive; } + RE::TESWeather* GetLockedWeather() const { return lockedWeather; } + + void PauseTime(); + void ResumeTime(); + bool IsTimePaused() const { return timePaused; } + + struct Settings + { + std::map recordMarkers = { + { "TO-DO", { 0.9f, 0.15, 0.15, 1 } }, + { "In-progress", { 0.5f, 0.8f, 0.0f, 1 } }, + { "Done", { 0.05f, 0.85f, 0.3f, 1 } } + }; + std::map markedRecords; + }; + + Settings settings; + +private: + void SaveAll(); + void SaveSettings(); + void LoadSettings(); + void ShowSettingsWindow(); + void Save(); + void Load(); + json j; + std::string settingsFilename = "EditorSettings"; + bool showSettingsWindow = false; +}; \ No newline at end of file diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp new file mode 100644 index 0000000000..e8dd2afe13 --- /dev/null +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -0,0 +1,26 @@ +#include "LightingTemplateWidget.h" + +LightingTemplateWidget::~LightingTemplateWidget() +{ +} + +void LightingTemplateWidget::DrawWidget() +{ + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) { + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("Menu")) { + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + } + ImGui::End(); +} + +void LightingTemplateWidget::LoadSettings() +{ +} + +void LightingTemplateWidget::SaveSettings() +{ +} \ No newline at end of file diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.h b/src/WeatherEditor/Weather/LightingTemplateWidget.h new file mode 100644 index 0000000000..bca2ed559c --- /dev/null +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../Widget.h" + +class LightingTemplateWidget : public Widget +{ +public: + RE::BGSLightingTemplate* lightingTemplate = nullptr; + + LightingTemplateWidget(RE::BGSLightingTemplate* a_lightingTemplate) + { + form = a_lightingTemplate; + lightingTemplate = a_lightingTemplate; + } + + ~LightingTemplateWidget(); + + virtual void DrawWidget() override; + virtual void LoadSettings() override; + virtual void SaveSettings() override; +}; \ No newline at end of file diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp new file mode 100644 index 0000000000..a1985ccc08 --- /dev/null +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -0,0 +1,640 @@ +#include "WeatherWidget.h" + +#include "../EditorWindow.h" + +#include + +#include "Feature.h" +#include "State.h" +#include "Util.h" +#include "WeatherManager.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Atmosphere, colorTimes) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DirectionalColor, max, min) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DALC, specular, fresnelPower, directional) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Settings, + parent, + inheritance, + weatherProperties, + weatherColors, + fogProperties, + + weatherColors, + dalc, + featureSettings) + +WeatherWidget::~WeatherWidget() +{ +} + +void WeatherWidget::DrawWidget() +{ + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_MenuBar)) { + DrawMenu(); + + auto editorWindow = EditorWindow::GetSingleton(); + bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; + + // Weather lock controls + if (isLocked) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); + ImGui::TextWrapped("WEATHER LOCKED - This weather is active and won't change with time"); + ImGui::PopStyleColor(); + if (ImGui::Button("Unlock Weather", ImVec2(-1, 0))) { + editorWindow->UnlockWeather(); + } + } else { + if (ImGui::Button("Lock & Force This Weather", ImVec2(-1, 0))) { + editorWindow->LockWeather(weather); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Force this weather to be active and prevent time-based weather changes"); + } + } + ImGui::Separator(); + + // Auto-apply toggle and manual buttons + ImGui::Checkbox("Auto-Apply Changes", &autoApply); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Automatically apply changes to the game as you edit"); + } + ImGui::SameLine(); + + // Time pause toggle + bool isPaused = editorWindow->IsTimePaused(); + if (isPaused) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); + if (ImGui::Button("Resume Time", ImVec2(ImGui::GetContentRegionAvail().x, 0))) { + editorWindow->ResumeTime(); + } + ImGui::PopStyleColor(2); + } else { + if (ImGui::Button("Pause Time", ImVec2(ImGui::GetContentRegionAvail().x, 0))) { + editorWindow->PauseTime(); + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Pause/resume time progression in game"); + } + + if (ImGui::Button("Apply Now", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { + ApplyChanges(); + } + ImGui::SameLine(); + if (ImGui::Button("Revert", ImVec2(-1, 0))) { + RevertChanges(); + } + ImGui::Separator(); + + auto& widgets = editorWindow->weatherWidgets; + + // Sets the parent widget if settings have been loaded. + if (settings.parent != "None") { + parent = GetParent(); + if (parent == nullptr) + settings.parent = "None"; + } + + if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { + // Option for "None" + if (ImGui::Selectable("None", parent == nullptr)) { + parent = nullptr; + settings.parent = "None"; + } + + for (int i = 0; i < widgets.size(); i++) { + auto& widget = widgets[i]; + + // Skip self-selection + if (widget == this) + continue; + + // Option for each widget + if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget)) { + parent = (WeatherWidget*)widget; + settings.parent = widget->GetEditorID(); + } + + // Set default focus to the current parent + if (parent == widget) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + if (parent && !parent->IsOpen()) { + ImGui::SameLine(); + if (ImGui::Button("Open")) + parent->SetOpen(true); + } + + // Tab bar for organizing settings + if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { + if (ImGui::BeginTabItem("Basic")) { + DrawProperties("Sun", { { "Sun Glare", INT8_SLIDER }, { "Sun Damage", INT8_SLIDER } }); + DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); + DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); + DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, + { "Thunder Lightning Frequency", INT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); + DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); + DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Lighting (DALC)")) { + DrawDALCSettings(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Atmosphere Colors")) { + DrawWeatherColorSettings(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Clouds")) { + DrawCloudSettings(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Fog")) { + DrawProperties("Fog", { { "Day Near", FLOAT_SLIDER }, { "Day Far", FLOAT_SLIDER }, { "Day Power", FLOAT_SLIDER }, { "Day Max", FLOAT_SLIDER }, + { "Night Near", FLOAT_SLIDER }, { "Night Far", FLOAT_SLIDER }, { "Night Power", FLOAT_SLIDER }, { "Night Max", FLOAT_SLIDER } }); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Features")) { + DrawFeatureSettings(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + } + ImGui::End(); +} + +void WeatherWidget::LoadSettings() +{ + if (!js.empty()) { + settings = js; + } + LoadFeatureSettings(); +} + +void WeatherWidget::SaveSettings() +{ + SaveFeatureSettings(); + js = settings; +} + +WeatherWidget* WeatherWidget::GetParent() +{ + auto editorWindow = EditorWindow::GetSingleton(); + auto& widgets = editorWindow->weatherWidgets; + + auto temp = std::find_if(widgets.begin(), widgets.end(), [&](Widget* w) { return w->GetEditorID() == settings.parent; }); + if (temp != widgets.end()) + return (WeatherWidget*)*temp; + + return nullptr; +} + +bool WeatherWidget::HasParent() const +{ + return settings.parent != "None"; +} + +void WeatherWidget::SetWeatherValues() +{ + std::map& weatherProps = settings.weatherProperties; + std::map& weatherColors = settings.weatherColors; + std::map& fogProperties = settings.fogProperties; + + auto& data = weather->data; + auto& colorData = weather->colorData; + auto& fogData = weather->fogData; + + weather->data.transDelta = (int8_t)weatherProps["Trans Delta"]; + + // Sun + data.sunGlare = (int8_t)weatherProps["Sun Glare"]; + data.sunDamage = (int8_t)weatherProps["Sun Damage"]; + + // Precipitation + data.precipitationBeginFadeIn = (int8_t)weatherProps["Precipitation Begin Fade In"]; + data.precipitationEndFadeOut = (int8_t)weatherProps["Precipitation End Fade Out"]; + + // Lightning + data.thunderLightningBeginFadeIn = (int8_t)weatherProps["Thunder Lightning Begin Fade In"]; + data.thunderLightningEndFadeOut = (int8_t)weatherProps["Thunder Lightning End Fade Out"]; + data.thunderLightningFrequency = (int8_t)weatherProps["Thunder Lightning Frequency"]; + Float3ToColor(weatherColors["Lightning Color"], weather->data.lightningColor); + + // Visual Effects + data.visualEffectBegin = (int8_t)weatherProps["Visual Effect Begin"]; + data.visualEffectEnd = (int8_t)weatherProps["Visual Effect End"]; + + // Wind + data.windSpeed = (uint8_t)weatherProps["Wind Speed"]; + data.windDirection = (int8_t)weatherProps["Wind Direction"]; + data.windDirectionRange = (int8_t)weatherProps["Wind Direction Range"]; + + // Fog + fogData.dayNear = fogProperties["Day Near"]; + fogData.dayFar = fogProperties["Day Far"]; + fogData.dayPower = fogProperties["Day Power"]; + fogData.dayMax = fogProperties["Day Max"]; + fogData.nightNear = fogProperties["Night Near"]; + fogData.nightFar = fogProperties["Night Far"]; + fogData.nightPower = fogProperties["Night Power"]; + fogData.nightMax = fogProperties["Night Max"]; + + // Atmosphere colors + for (size_t i = 0; i < ColorTypes::kTotal; i++) { + for (size_t j = 0; j < ColorTimes::kTotal; j++) { + auto& color = colorData[i][j]; + Float3ToColor(settings.atmosphereColors[i].colorTimes[j], color); + } + } + + //DALC + for (size_t i = 0; i < ColorTimes::kTotal; i++) { + auto& dalc = weather->directionalAmbientLightingColors[i]; + auto& settingsDalc = settings.dalc[i]; + dalc.fresnelPower = settingsDalc.fresnelPower; + + Float3ToColor(settingsDalc.specular, dalc.specular); + + Float3ToColor(settingsDalc.directional[0].max, dalc.directional.x.max); + Float3ToColor(settingsDalc.directional[0].min, dalc.directional.x.min); + + Float3ToColor(settingsDalc.directional[1].max, dalc.directional.y.max); + Float3ToColor(settingsDalc.directional[1].min, dalc.directional.y.min); + + Float3ToColor(settingsDalc.directional[2].max, dalc.directional.z.max); + Float3ToColor(settingsDalc.directional[2].min, dalc.directional.z.min); + } + + // Clouds + for (size_t i = 0; i < TESWeather::kTotalLayers; i++) { + auto& settingsCloud = settings.clouds[i]; + + weather->cloudLayerSpeedX[i] = (int8_t)settingsCloud.cloudLayerSpeedX; + weather->cloudLayerSpeedY[i] = (int8_t)settingsCloud.cloudLayerSpeedY; + + auto& cloudColors = weather->cloudColorData[i]; + auto& cloudAlphas = weather->cloudAlpha[i]; + + for (int j = 0; j < ColorTimes::kTotal; j++) { + cloudAlphas[j] = settingsCloud.cloudAlpha[j]; + Float3ToColor(settingsCloud.color[j], cloudColors[j]); + } + } +} + +void WeatherWidget::LoadWeatherValues() +{ + std::map& weatherProps = settings.weatherProperties; + std::map& weatherColors = settings.weatherColors; + std::map& fogProperties = settings.fogProperties; + + const auto& data = weather->data; + const auto& colorData = weather->colorData; + const auto& fogData = weather->fogData; + + weatherProps["Trans Delta"] = data.transDelta; + + // Sun + weatherProps["Sun Glare"] = data.sunGlare; + weatherProps["Sun Damage"] = data.sunDamage; + + // Precipitation + weatherProps["Precipitation Begin Fade In"] = data.precipitationBeginFadeIn; + weatherProps["Precipitation End Fade Out"] = data.precipitationEndFadeOut; + + // Lightning + weatherProps["Thunder Lightning Begin Fade In"] = data.thunderLightningBeginFadeIn; + weatherProps["Thunder Lightning End Fade Out"] = data.thunderLightningEndFadeOut; + weatherProps["Thunder Lightning Frequency"] = data.thunderLightningFrequency; + ColorToFloat3(data.lightningColor, weatherColors["Lightning Color"]); + + // Visual Effects + weatherProps["Visual Effect Begin"] = data.visualEffectBegin; + weatherProps["Visual Effect End"] = data.visualEffectEnd; + + // Wind + weatherProps["Wind Speed"] = data.windSpeed; + weatherProps["Wind Direction"] = data.windDirection; + weatherProps["Wind Direction Range"] = data.windDirectionRange; + + // Fog + fogProperties["Day Near"] = fogData.dayNear; + fogProperties["Day Far"] = fogData.dayFar; + fogProperties["Night Near"] = fogData.nightNear; + fogProperties["Night Far"] = fogData.nightFar; + fogProperties["Day Power"] = fogData.dayPower; + fogProperties["Night Power"] = fogData.nightPower; + fogProperties["Day Max"] = fogData.dayMax; + fogProperties["Night Max"] = fogData.nightMax; + + // Atmosphere color + for (size_t i = 0; i < ColorTypes::kTotal; i++) { + for (size_t j = 0; j < ColorTimes::kTotal; j++) { + auto& color = colorData[i][j]; + ColorToFloat3(color, settings.atmosphereColors[i].colorTimes[j]); + } + } + + // DALC + for (size_t i = 0; i < ColorTimes::kTotal; i++) { + auto& dalc = weather->directionalAmbientLightingColors[i]; + auto& settingsDalc = settings.dalc[i]; + dalc.fresnelPower = settingsDalc.fresnelPower; + + ColorToFloat3(dalc.specular, settingsDalc.specular); + + ColorToFloat3(dalc.directional.x.max, settingsDalc.directional[0].max); + ColorToFloat3(dalc.directional.x.min, settingsDalc.directional[0].min); + + ColorToFloat3(dalc.directional.y.max, settingsDalc.directional[1].max); + ColorToFloat3(dalc.directional.y.min, settingsDalc.directional[1].min); + + ColorToFloat3(dalc.directional.z.max, settingsDalc.directional[2].max); + ColorToFloat3(dalc.directional.z.min, settingsDalc.directional[2].min); + } + + // Clouds + for (size_t i = 0; i < TESWeather::kTotalLayers; i++) { + auto& settingsCloud = settings.clouds[i]; + + settingsCloud.cloudLayerSpeedX = weather->cloudLayerSpeedX[i]; + settingsCloud.cloudLayerSpeedY = weather->cloudLayerSpeedY[i]; + + auto& cloudColors = weather->cloudColorData[i]; + auto& cloudAlphas = weather->cloudAlpha[i]; + + for (int j = 0; j < ColorTimes::kTotal; j++) { + settingsCloud.cloudAlpha[j] = cloudAlphas[j]; + ColorToFloat3(cloudColors[j], settingsCloud.color[j]); + } + } +} + +void WeatherWidget::DrawDALCSettings() +{ + if (ImGui::CollapsingHeader("DALC settings", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + bool& doesInherit = settings.inheritance["DALC"]; + ImGui::Checkbox("Inherit From Parent##dalc", &doesInherit); + + if (doesInherit && HasParent()) { + for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { + settings.dalc[i] = GetParent()->settings.dalc[i]; + } + } else { + doesInherit = false; + bool changed = false; + for (int i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { + std::string label = ColorTimeLabel(i); + + if (ImGui::CollapsingHeader(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + ImGui::Spacing(); + if (DrawColorEdit(std::format("Specular##{}", label), settings.dalc[i].specular)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat(std::format("Fresnel Power##{}", label), settings.dalc[i].fresnelPower)) changed = true; + ImGui::Spacing(); + + for (int j = 0; j < 3; j++) { + if (DrawColorEdit(std::format("DALC X Max##{}", label), settings.dalc[i].directional[j].max)) changed = true; + if (DrawColorEdit(std::format("DALC X Min##{}", label), settings.dalc[i].directional[j].min)) changed = true; + ImGui::Spacing(); + } + } + } + if (changed && autoApply) { + ApplyChanges(); + } + } + } +} + +void WeatherWidget::DrawWeatherColorSettings() +{ + if (ImGui::CollapsingHeader("Atmosphere Colors", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + bool& doesInherit = settings.inheritance["Atmosphere Colors"]; + ImGui::Checkbox("Inherit From Parent##atmosphereColors", &doesInherit); + + if (&doesInherit && HasParent()) { + for (size_t i = 0; i < ColorTypes::kTotal; i++) { + settings.atmosphereColors[i] = GetParent()->settings.atmosphereColors[i]; + } + } else { + doesInherit = false; + bool changed = false; + + for (int i = 0; i < ColorTypes::kTotal; i++) { + std::string colorTypeLabel = ColorTypeLabel(i); + + if (ImGui::CollapsingHeader(colorTypeLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + for (int j = 0; j < ColorTimes::kTotal; j++) { + if (DrawColorEdit(std::format("{}##{}", ColorTimeLabel(j), colorTypeLabel), settings.atmosphereColors[i].colorTimes[j])) changed = true; + ImGui::Spacing(); + } + } + } + + if (changed && autoApply) { + ApplyChanges(); + } + } + } +} + +void WeatherWidget::DrawCloudSettings() +{ + if (ImGui::CollapsingHeader("Clouds Properties", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + bool& doesInherit = settings.inheritance["Clouds"]; + ImGui::Checkbox("Inherit From Parent##cloud", &doesInherit); + + if (doesInherit && HasParent()) { + for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { + settings.dalc[i] = GetParent()->settings.dalc[i]; + } + } else { + doesInherit = false; + bool changed = false; + for (int i = 0; i < TESWeather::kTotalLayers; i++) { + std::string layer = std::format("Layer {}", i); + + if (ImGui::CollapsingHeader(layer.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; + ImGui::Spacing(); + if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; + + for (int j = 0; j < ColorTimes::kTotal; j++) { + std::string colorTime = ColorTimeLabel(j).c_str(); + + if (ImGui::CollapsingHeader(colorTime.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (DrawColorEdit(std::format("Cloud Color##{}{}", colorTime, i), settings.clouds[i].color[j])) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat(std::format("Cloud Alpha##{}{}", colorTime, i), settings.clouds[i].cloudAlpha[j])) changed = true; + ImGui::Spacing(); + } + } + } + } + if (changed && autoApply) { + ApplyChanges(); + } + } + } +} + +void WeatherWidget::DrawProperties(std::string category, std::map properties) +{ + if (ImGui::CollapsingHeader(std::format("{} Properties", category).c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + bool& doesInherit = settings.inheritance[category]; + ImGui::Checkbox(std::format("Inherit From Parent##{}", category).c_str(), &doesInherit); + + if (doesInherit && HasParent()) { + for (auto& p : properties) { + InheritFromParent(p.first); + } + } else { + doesInherit = false; + bool changed = false; + + for (auto& p : properties) { + switch (p.second) { + case 0: + if (DrawSliderInt8(p.first, settings.weatherProperties[p.first])) changed = true; + break; + case 1: + if (DrawColorEdit(p.first, settings.weatherColors[p.first])) changed = true; + break; + case 2: + if (DrawSliderUint8(p.first, settings.weatherProperties[p.first])) changed = true; + break; + case 3: + if (DrawSliderFloat(p.first, settings.fogProperties[p.first])) changed = true; + break; + default: + break; + } + } + + if (changed && autoApply) { + ApplyChanges(); + } + } + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); +} + +void WeatherWidget::InheritFromParent(const std::string& property) +{ + if (settings.weatherProperties.find(property) != settings.weatherProperties.end()) { + settings.weatherProperties[property] = GetParent()->settings.weatherProperties[property]; + } else if (settings.weatherColors.find(property) != settings.weatherColors.end()) { + settings.weatherColors[property] = GetParent()->settings.weatherColors[property]; + } +} + +void WeatherWidget::SaveFeatureSettings() +{ + auto* weatherManager = WeatherManager::GetSingleton(); + + for (const auto& [featureName, featureJson] : settings.featureSettings) { + if (!featureJson.empty()) { + weatherManager->SaveSettingsToWeather(weather, featureName, featureJson); + } + } +} + +void WeatherWidget::LoadFeatureSettings() +{ + auto* weatherManager = WeatherManager::GetSingleton(); + + for (auto* feature : Feature::GetFeatureList()) { + if (!feature || !feature->loaded || !feature->SupportsWeather()) { + continue; + } + + json featureJson; + if (weatherManager->LoadSettingsFromWeather(weather, feature->GetShortName(), featureJson)) { + settings.featureSettings[feature->GetShortName()] = featureJson; + } + } +} + +void WeatherWidget::ApplyChanges() +{ + SetWeatherValues(); + logger::info("Applied changes to weather: {}", GetEditorID()); +} + +void WeatherWidget::RevertChanges() +{ + LoadWeatherValues(); + logger::info("Reverted changes for weather: {}", GetEditorID()); +} + +void WeatherWidget::DrawFeatureSettings() +{ + if (ImGui::CollapsingHeader("Per-Feature Weather Settings", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + ImGui::TextWrapped("Configure feature-specific settings that will be applied when this weather is active."); + ImGui::Spacing(); + + for (auto* feature : Feature::GetFeatureList()) { + if (!feature || !feature->loaded || !feature->SupportsWeather()) { + continue; + } + + std::string featureName = feature->GetShortName(); + std::string displayName = feature->GetName(); + + if (ImGui::TreeNode(displayName.c_str())) { + ImGui::Text("Feature: %s", featureName.c_str()); + ImGui::Spacing(); + + // Show if settings exist for this feature + bool hasSettings = settings.featureSettings.find(featureName) != settings.featureSettings.end() && + !settings.featureSettings[featureName].empty(); + + if (hasSettings) { + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); + + if (ImGui::Button("Clear Settings")) { + settings.featureSettings[featureName] = json::object(); + } + ImGui::SameLine(); + if (ImGui::Button("View JSON")) { + ImGui::OpenPopup("FeatureJSON"); + } + + if (ImGui::BeginPopup("FeatureJSON")) { + ImGui::Text("Settings JSON:"); + ImGui::Separator(); + std::string jsonStr = settings.featureSettings[featureName].dump(2); + ImGui::TextWrapped("%s", jsonStr.c_str()); + ImGui::EndPopup(); + } + } else { + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "No weather-specific settings"); + } + + ImGui::Spacing(); + ImGui::TextWrapped("Note: Feature settings should be configured through the feature's own settings panel. " + "This section shows which features have per-weather overrides."); + + ImGui::TreePop(); + } + } + } +} diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h new file mode 100644 index 0000000000..d33a4a814a --- /dev/null +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -0,0 +1,92 @@ +#pragma once + +#include "../Widget.h" + +using TESWeather = RE::TESWeather; +using ColorTypes = TESWeather::ColorTypes; +using ColorTimes = TESWeather::ColorTimes; +using FogData = TESWeather::FogData; + +class WeatherWidget : public Widget +{ +public: + WeatherWidget* parent = nullptr; + TESWeather* weather = nullptr; + + WeatherWidget(TESWeather* a_weather) + { + form = a_weather; + weather = a_weather; + LoadWeatherValues(); + } + + struct DirectionalColor + { + float3 min; + float3 max; + }; + + struct DALC + { + DirectionalColor directional[3]; + float3 specular; + float fresnelPower; + }; + + struct Atmosphere + { + float3 colorTimes[ColorTimes::kTotal]; + }; + + struct Cloud + { + int cloudLayerSpeedY; + int cloudLayerSpeedX; + float3 color[ColorTimes::kTotal]; + float cloudAlpha[ColorTimes::kTotal]; + }; + + struct Settings + { + std::string parent = "None"; + std::map inheritance; + std::map weatherProperties; + std::map weatherColors; + std::map fogProperties; + + Atmosphere atmosphereColors[ColorTypes::kTotal]; + DALC dalc[ColorTimes::kTotal]; + Cloud clouds[TESWeather::kTotalLayers]; + + // Per-feature settings storage + std::map featureSettings; + }; + + Settings settings; + bool autoApply = true; // Auto-apply changes to game in real-time + + ~WeatherWidget(); + + virtual void DrawWidget() override; + virtual void LoadSettings() override; + virtual void SaveSettings() override; + + WeatherWidget* GetParent(); + bool HasParent() const; + void SetWeatherValues(); + void LoadWeatherValues(); + void ApplyChanges(); + void RevertChanges(); + + // New methods for per-feature settings + void SaveFeatureSettings(); + void LoadFeatureSettings(); + +private: + void DrawDALCSettings(); + void DrawWeatherColorSettings(); + void DrawCloudSettings(); + void DrawFeatureSettings(); + void DrawProperties(std::string category, std::map properties); + void InheritFromParent(const std::string& property); +}; \ No newline at end of file diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp new file mode 100644 index 0000000000..c6304f079e --- /dev/null +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp @@ -0,0 +1,28 @@ +#include "WorldSpaceWidget.h" +#include "Util.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(WorldSpaceWidget::Settings, temp) + +WorldSpaceWidget::~WorldSpaceWidget() +{ +} + +void WorldSpaceWidget::DrawWidget() +{ + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) { + DrawMenu(); + } + ImGui::End(); +} + +void WorldSpaceWidget::LoadSettings() +{ + if (!js.empty()) { + settings = js; + } +} + +void WorldSpaceWidget::SaveSettings() +{ + js = settings; +} diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.h b/src/WeatherEditor/Weather/WorldSpaceWidget.h new file mode 100644 index 0000000000..a336f6fa6e --- /dev/null +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.h @@ -0,0 +1,28 @@ +#pragma once + +#include "../Widget.h" + +class WorldSpaceWidget : public Widget +{ +public: + RE::TESWorldSpace* worldSpace = nullptr; + + WorldSpaceWidget(RE::TESWorldSpace* a_worldspace) + { + form = a_worldspace; + worldSpace = a_worldspace; + } + + ~WorldSpaceWidget(); + + virtual void DrawWidget() override; + virtual void LoadSettings() override; + virtual void SaveSettings() override; + + struct Settings + { + int temp; // Temp var to resolve macro issue in cpp file as worldspace has no settings at the moment. Can be removed once settings are added. + }; + + Settings settings; +}; \ No newline at end of file diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp new file mode 100644 index 0000000000..d65195a222 --- /dev/null +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -0,0 +1,152 @@ +#include "WeatherUtils.h" + +float Int8ToFloat(const int8_t& value) +{ + return ((float)(value + 128) / 255.0f); +} + +float Uint8ToFloat(const uint8_t& value) +{ + return ((float)(value) / 255.0f); +} + +int8_t FloatToInt8(const float& value) +{ + return (int8_t)std::lerp(-128, 127, value); +} + +uint8_t FloatToUint8(const float& value) +{ + return (uint8_t)std::lerp(0, 255, value); +} + +void Float3ToColor(const float3& f3, RE::Color& color) +{ + color.red = FloatToUint8(f3.x); + color.green = FloatToUint8(f3.y); + color.blue = FloatToUint8(f3.z); +} + +void Float3ToColor(const float3& f3, RE::TESWeather::Data::Color3& color) +{ + color.red = FloatToInt8(f3.x); + color.green = FloatToInt8(f3.y); + color.blue = FloatToInt8(f3.z); +} + +void ColorToFloat3(const RE::Color& color, float3& f3) +{ + f3.x = Uint8ToFloat(color.red); + f3.y = Uint8ToFloat(color.green); + f3.z = Uint8ToFloat(color.blue); +} + +void ColorToFloat3(const RE::TESWeather::Data::Color3& color, float3& f3) +{ + f3.x = Int8ToFloat(color.red); + f3.y = Int8ToFloat(color.green); + f3.z = Int8ToFloat(color.blue); +} + +std::string ColorTimeLabel(const int i) +{ + std::string label = ""; + switch (i) { + case 0: + label = "Sunrise"; + break; + case 1: + label = "Day"; + break; + case 2: + label = "Sunset"; + break; + case 3: + label = "Night"; + break; + default: + break; + } + return label; +} + +std::string ColorTypeLabel(const int i) +{ + std::string label = ""; + switch (i) { + case 0: + label = "Sky Upper"; + break; + case 1: + label = "Fog Near"; + break; + case 2: + label = "Unknown"; + break; + case 3: + label = "Ambient"; + break; + case 4: + label = "Sunlight"; + break; + case 5: + label = "Sun"; + break; + case 6: + label = "Stars"; + break; + case 7: + label = "Sky Lower"; + break; + case 8: + label = "Horizon"; + break; + case 9: + label = "Effect Lighting"; + break; + case 10: + label = "Cloud LOD Diffuse"; + break; + case 11: + label = "Cloud LOD Ambient"; + break; + case 12: + label = "Fog Far"; + break; + case 13: + label = "Sky Statics"; + break; + case 14: + label = "Water Multiplier"; + break; + case 15: + label = "Sun Glare"; + break; + case 16: + label = "Moon Glare"; + break; + default: + break; + } + return label; +} + +bool DrawSliderInt8(const std::string& label, int& property) +{ + return ImGui::SliderInt(label.c_str(), &property, -128, 127); +} + +bool DrawColorEdit(const std::string& l, float3& property) +{ + return ImGui::ColorEdit3(l.c_str(), (float*)&property); +} + +bool DrawSliderUint8(const std::string& label, int& property) +{ + return ImGui::SliderInt(label.c_str(), &property, 0, 255); +} + +bool DrawSliderFloat(const std::string& label, float& property) +{ + return ImGui::SliderFloat(label.c_str(), &property, 0, 50000); +} \ No newline at end of file diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h new file mode 100644 index 0000000000..80f5381d12 --- /dev/null +++ b/src/WeatherEditor/WeatherUtils.h @@ -0,0 +1,23 @@ +#include "Util.h" + +void Float3ToColor(const float3& newColor, RE::Color& color); +void Float3ToColor(const float3& newColor, RE::TESWeather::Data::Color3& color); + +void ColorToFloat3(const RE::Color& color, float3& newColor); +void ColorToFloat3(const RE::TESWeather::Data::Color3& color, float3& newColor); + +std::string ColorTimeLabel(const int i); +std::string ColorTypeLabel(const int i); + +bool DrawSliderInt8(const std::string& label, int& property); +bool DrawColorEdit(const std::string& l, float3& property); +bool DrawSliderUint8(const std::string& label, int& property); +bool DrawSliderFloat(const std::string& label, float& property); + +enum ControlType +{ + INT8_SLIDER = 0, + COLOR3_PICKER, + UINT8_SLIDER, + FLOAT_SLIDER +}; \ No newline at end of file diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp new file mode 100644 index 0000000000..619a3ad151 --- /dev/null +++ b/src/WeatherEditor/Widget.cpp @@ -0,0 +1,98 @@ +#include "Widget.h" +#include "State.h" +#include "Util.h" + +void Widget::Save() +{ + SaveSettings(); + const std::string filePath = std::format("{}\\{}", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName()); + const std::string file = std::format("{}\\{}.json", filePath, GetEditorID()); + + std::ofstream settingsFile(file); + if (!std::filesystem::exists(filePath) || !std::filesystem::is_directory(filePath)) { + try { + std::filesystem::create_directories(filePath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error creating directory during Save ({}) : {}\n", filePath, e.what()); + return; + } + } + + if (!settingsFile.good() || !settingsFile.is_open()) { + logger::warn("Failed to open settings file: {}", file); + return; + } + + if (settingsFile.fail()) { + logger::warn("Unable to create settings file: {}", file); + settingsFile.close(); + return; + } + + logger::info("Saving settings file: {}", file); + + try { + settingsFile << js.dump(1); + + settingsFile.close(); + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing settings for settings file ({}) : {}\n", filePath, e.what()); + settingsFile.close(); + } +} + +void Widget::Load() +{ + std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName(), GetEditorID()); + + std::ifstream settingsFile(filePath); + + if (!std::filesystem::exists(filePath)) { + // Does not have any settings so just return. + return; + } + + if (!settingsFile.good() || !settingsFile.is_open()) { + logger::warn("Failed to load settings file: {}", filePath); + return; + } + + try { + js << settingsFile; + settingsFile.close(); + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing settings for file ({}) : {}\n", filePath, e.what()); + settingsFile.close(); + } + LoadSettings(); +} + +void Widget::DrawMenu() +{ + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("Menu")) { + if (ImGui::MenuItem("Save")) { + Save(); + } + if (ImGui::MenuItem("Load")) { + Load(); + } + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } +} + +std::string Widget::GetFolderName() +{ + switch (form->GetFormType()) { + case RE::FormType::Weather: + return "Weathers"; + case RE::FormType::LightingMaster: + return "LightingTemplates"; + case RE::FormType::WorldSpace: + return "WorldSpaces"; + default: + return "Unknown"; + } +} diff --git a/src/WeatherEditor/Widget.h b/src/WeatherEditor/Widget.h new file mode 100644 index 0000000000..3bdad93b39 --- /dev/null +++ b/src/WeatherEditor/Widget.h @@ -0,0 +1,72 @@ + +#pragma once + +#include "Util.h" + +class WidgetSharedData +{ +private: + int uniqueID = 0; + +public: + static WidgetSharedData* GetSingleton() + { + static WidgetSharedData sharedData; + return &sharedData; + } + + int GetNewID() + { + return -uniqueID++; + } +}; + +class Widget +{ +public: + RE::TESForm* form = nullptr; + + virtual ~Widget() {}; + + virtual std::string GetEditorID() + { + return form->GetFormEditorID(); + } + + virtual std::string GetFormID() + { + return std::format("{:08X}", form->GetFormID()); + } + + virtual std::string GetFilename() + { + if (auto file = form->GetFile()) + return std::format("{}", file->GetFilename()); + return "Generated"; + } + + virtual void DrawWidget() = 0; + + bool open = false; + + bool IsOpen() const + { + return open; + } + + void SetOpen(bool state = true) + { + open = state; + } + + void Save(); + void Load(); + + virtual void LoadSettings() = 0; + virtual void SaveSettings() = 0; + +protected: + json js = json(); + virtual void DrawMenu(); + std::string GetFolderName(); +}; \ No newline at end of file diff --git a/src/WeatherManager.cpp b/src/WeatherManager.cpp new file mode 100644 index 0000000000..74c48ecf04 --- /dev/null +++ b/src/WeatherManager.cpp @@ -0,0 +1,216 @@ +#include "WeatherManager.h" + +#include "State.h" + +WeatherManager::CurrentWeathers WeatherManager::GetCurrentWeathers() +{ + CurrentWeathers result; + + auto sky = RE::Sky::GetSingleton(); + if (!sky) { + return result; + } + + result.currentWeather = sky->currentWeather; + result.lastWeather = sky->lastWeather; + result.lerpFactor = sky->currentWeatherPct; + + return result; +} + +void WeatherManager::LoadPerWeatherSettingsFromDisk() +{ + const std::string weathersPath = std::format("{}\\Weathers", Util::PathHelpers::GetCommunityShaderPath().string()); + + if (!std::filesystem::exists(weathersPath) || !std::filesystem::is_directory(weathersPath)) { + logger::info("Weathers directory does not exist: {}", weathersPath); + return; + } + + logger::info("Loading per-weather settings from: {}", weathersPath); + + for (const auto& entry : std::filesystem::directory_iterator(weathersPath)) { + if (!entry.is_regular_file() || entry.path().extension() != ".json") { + continue; + } + + std::string weatherKey = entry.path().stem().string(); + std::ifstream settingsFile(entry.path()); + + if (!settingsFile.good() || !settingsFile.is_open()) { + logger::warn("Failed to open weather settings file: {}", entry.path().string()); + continue; + } + + try { + json weatherData; + settingsFile >> weatherData; + settingsFile.close(); + + // Store the entire weather settings in cache + // The structure is expected to be: { "FeatureName": { settings }, ... } + if (weatherData.is_object()) { + for (auto& [featureName, featureSettings] : weatherData.items()) { + perWeatherSettingsCache[weatherKey][featureName] = featureSettings; + } + logger::info("Loaded settings for weather: {}", weatherKey); + } + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing weather settings file ({}): {}", entry.path().string(), e.what()); + } + } + + logger::info("Finished loading per-weather settings. Total weathers: {}", perWeatherSettingsCache.size()); +} + +void WeatherManager::UpdateFeatures() +{ + auto currentWeathers = GetCurrentWeathers(); + + // Check if weather state has changed + bool weatherChanged = (currentWeathers.currentWeather != lastKnownWeather.currentWeather) || + (currentWeathers.lastWeather != lastKnownWeather.lastWeather); + + // Always update if lerp factor changes or weather changed + if (weatherChanged || std::abs(currentWeathers.lerpFactor - lastKnownWeather.lerpFactor) > 0.001f) { + // Get all features and update those that support weather + for (auto* feature : Feature::GetFeatureList()) { + if (!feature || !feature->loaded) { + continue; + } + + // Check if feature supports weather (will be added to Feature class) + if (feature->SupportsWeather()) { + json currWeatherSettings; + json nextWeatherSettings; + + // Load settings for current weather + if (currentWeathers.currentWeather) { + LoadSettingsFromWeather(currentWeathers.currentWeather, feature->GetShortName(), currWeatherSettings); + } + + // Load settings for transitioning weather + if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { + LoadSettingsFromWeather(currentWeathers.lastWeather, feature->GetShortName(), nextWeatherSettings); + } + + // Let the feature update its settings based on weather transition + feature->UpdateSettingsFromWeathers(currWeatherSettings, nextWeatherSettings, currentWeathers.lerpFactor); + } + } + + lastKnownWeather = currentWeathers; + } +} + +void WeatherManager::SaveSettingsToWeather(RE::TESWeather* weather, const std::string& featureName, const json& settings) +{ + if (!weather) { + return; + } + + std::string weatherKey = GetWeatherKey(weather); + + // Update cache + perWeatherSettingsCache[weatherKey][featureName] = settings; + + // Save to disk + const std::string weathersPath = std::format("{}\\Weathers", Util::PathHelpers::GetCommunityShaderPath().string()); + const std::string filePath = std::format("{}\\{}.json", weathersPath, weatherKey); + + // Create directory if needed + if (!std::filesystem::exists(weathersPath)) { + try { + std::filesystem::create_directories(weathersPath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error creating Weathers directory ({}): {}", weathersPath, e.what()); + return; + } + } + + // Load existing weather data if it exists + json weatherData; + if (std::filesystem::exists(filePath)) { + std::ifstream existingFile(filePath); + if (existingFile.good() && existingFile.is_open()) { + try { + existingFile >> weatherData; + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing existing weather file ({}): {}", filePath, e.what()); + weatherData = json::object(); + } + existingFile.close(); + } + } + + // Update with new feature settings + weatherData[featureName] = settings; + + // Write back to disk + std::ofstream settingsFile(filePath); + if (!settingsFile.good() || !settingsFile.is_open()) { + logger::warn("Failed to open weather settings file for writing: {}", filePath); + return; + } + + try { + settingsFile << weatherData.dump(1); + settingsFile.close(); + logger::info("Saved {} settings for weather: {}", featureName, weatherKey); + } catch (const std::exception& e) { + logger::warn("Error writing weather settings file ({}): {}", filePath, e.what()); + } +} + +bool WeatherManager::LoadSettingsFromWeather(RE::TESWeather* weather, const std::string& featureName, json& o_json) +{ + if (!weather) { + return false; + } + + std::string weatherKey = GetWeatherKey(weather); + + // Check cache first + auto weatherIt = perWeatherSettingsCache.find(weatherKey); + if (weatherIt != perWeatherSettingsCache.end()) { + auto featureIt = weatherIt->second.find(featureName); + if (featureIt != weatherIt->second.end()) { + o_json = featureIt->second; + return true; + } + } + + return false; +} + +std::string WeatherManager::GetWeatherKey(RE::TESWeather* weather) +{ + if (!weather) { + return "None"; + } + + const char* editorID = weather->GetFormEditorID(); + if (editorID && editorID[0] != '\0') { + return std::string(editorID); + } + + // Fallback to FormID if no EditorID + return std::format("{:08X}", weather->GetFormID()); +} + +bool WeatherManager::HasWeatherSettings(RE::TESWeather* weather) const +{ + if (!weather) { + return false; + } + + std::string weatherKey = GetWeatherKey(weather); + return perWeatherSettingsCache.find(weatherKey) != perWeatherSettingsCache.end(); +} + +void WeatherManager::ClearCache() +{ + perWeatherSettingsCache.clear(); + lastKnownWeather = CurrentWeathers(); + logger::info("Cleared WeatherManager cache"); +} diff --git a/src/WeatherManager.h b/src/WeatherManager.h new file mode 100644 index 0000000000..06305d4610 --- /dev/null +++ b/src/WeatherManager.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +using json = nlohmann::json; + +class WeatherManager +{ +public: + static WeatherManager* GetSingleton() + { + static WeatherManager singleton; + return &singleton; + } + + struct CurrentWeathers + { + RE::TESWeather* currentWeather = nullptr; + RE::TESWeather* lastWeather = nullptr; + float lerpFactor = 0.0f; + }; + + // Get current weather state and transition info + CurrentWeathers GetCurrentWeathers(); + + // Load all per-weather settings from disk into cache + void LoadPerWeatherSettingsFromDisk(); + + // Per-frame update - notify features of weather changes + void UpdateFeatures(); + + // Save feature settings for a specific weather + void SaveSettingsToWeather(RE::TESWeather* weather, const std::string& featureName, const json& settings); + + // Load feature settings for a specific weather + bool LoadSettingsFromWeather(RE::TESWeather* weather, const std::string& featureName, json& o_json); + + // Get the weather key for caching + static std::string GetWeatherKey(RE::TESWeather* weather); + + // Check if settings exist for a weather + bool HasWeatherSettings(RE::TESWeather* weather) const; + + // Clear all cached settings + void ClearCache(); + +private: + WeatherManager() = default; + ~WeatherManager() = default; + WeatherManager(const WeatherManager&) = delete; + WeatherManager& operator=(const WeatherManager&) = delete; + + // Cache of all loaded per-weather settings: weatherKey -> featureName -> settings + std::map> perWeatherSettingsCache; + + // Track last known weather state to detect changes + CurrentWeathers lastKnownWeather; +}; diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index c3873b3423..d4d6b6c9ed 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -8,6 +8,7 @@ #include "ShaderCache.h" #include "State.h" #include "TruePBR.h" +#include "WeatherEditor/EditorWindow.h" #include "ENB/ENBSeriesAPI.h" @@ -110,6 +111,8 @@ void MessageHandler(SKSE::MessagingInterface::Message* message) RE::DebugMessageBox(std::format("Community Shaders\n{}, will disable all hooks and features", errorMessage).c_str()); } + EditorWindow::GetSingleton()->SetupResources(); + if (errors.empty()) { globals::OnDataLoaded(); EngineFix::InstallOnDataLoadedFixes(); From 60078df9b6738ccf7e2c71dcc2fd299423ef361f Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 8 Dec 2025 16:18:17 +1000 Subject: [PATCH 02/30] better custom weather system, md writeups, fixes --- .../WeatherVariableRegistration.md | 265 ++++++++++++++++++ .../examples/ExampleWeatherFeature.cpp | 115 ++++++++ .../examples/ExampleWeatherFeature.h | 30 ++ .../Shaders/WeatherEditor/DiffuseIBLCS.hlsl | 45 +++ src/Feature.h | 17 +- src/Features/WeatherEditor.cpp | 4 +- src/State.cpp | 3 + src/WeatherEditor/EditorWindow.cpp | 77 ++++- src/WeatherEditor/EditorWindow.h | 8 + src/WeatherEditor/Weather/WeatherWidget.cpp | 116 +++++--- src/WeatherEditor/Weather/WeatherWidget.h | 1 - .../Weather/WorldSpaceWidget.cpp | 1 - src/WeatherManager.cpp | 18 +- src/WeatherManager.h | 1 + src/WeatherVariableRegistry.h | 256 +++++++++++++++++ 15 files changed, 893 insertions(+), 64 deletions(-) create mode 100644 docs/weather-system-docs/WeatherVariableRegistration.md create mode 100644 docs/weather-system-docs/examples/ExampleWeatherFeature.cpp create mode 100644 docs/weather-system-docs/examples/ExampleWeatherFeature.h create mode 100644 features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl create mode 100644 src/WeatherVariableRegistry.h diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md new file mode 100644 index 0000000000..d4fa81cb90 --- /dev/null +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -0,0 +1,265 @@ +# Weather Variable Registration System + +## Overview + +The weather variable registration system provides a centralized way for features to support per-weather settings. Features register their variables once during initialization, and the weather system automatically handles serialization, interpolation, and state management. + +## Architecture + +### Core Components + +**WeatherVariableRegistry.h** contains: + +- **`IWeatherVariable`**: Base interface for all weather variables +- **`WeatherVariable`**: Templated variable with type-safe serialization and interpolation +- **`FloatVariable`, `Float3Variable`, `Float4Variable`**: Specialized types with range support +- **`FeatureWeatherRegistry`**: Manages all variables for a single feature +- **`GlobalWeatherRegistry`**: Singleton coordinating all features + +### Data Flow + +``` +Feature Registration → Global Registry → Weather Manager + ↓ ↓ ↓ + RegisterWeatherVariables Tracks Support Detects Changes + ↓ ↓ ↓ + Variable Metadata Per-Feature Loads JSON + Registry ↓ + Interpolates + ↓ + Updates Variables +``` + +## Usage Guide + +### Implementing Weather Support in Features + +#### Step 1: Define Your Settings Structure + +In your feature class, override `RegisterWeatherVariables()`: + +```cpp +class MyFeature : public Feature +{ + struct Settings + { + float intensity = 1.0f; + float3 color = { 1.0f, 1.0f, 1.0f }; + bool enabled = true; + } settings; + + void RegisterWeatherVariables() override + { + // ... rest of feature implementation +}; +``` + +#### Step 2: Override RegisterWeatherVariables() + +```cpp +void MyFeature::RegisterWeatherVariables() override +{ + auto* registry = WeatherVariables::GlobalWeatherRegistry::GetSingleton() + ->GetOrCreateFeatureRegistry(GetShortName()); + + // Register a float with range constraints + registry->RegisterVariable(std::make_shared( + "Intensity", // JSON key + "Effect Intensity", // Display name + "Controls the strength", // Tooltip + &settings.intensity, // Pointer to variable + 1.0f, // Default value + 0.0f, 2.0f // Min/max range + )); + + // Register a float3 (color or vector) + registry->RegisterVariable(std::make_shared( + "Color", + "Effect Color", + "RGB color values", + &settings.color, + float3{ 1.0f, 1.0f, 1.0f } + )); + + // Register bool with custom interpolation + registry->RegisterVariable(std::make_shared>( + "Enabled", + "Enable Effect", + "Toggle the effect", + &settings.enabled, + true, + [](const bool& from, const bool& to, float factor) { + return factor > 0.5f ? to : from; // Switch at midpoint + } + )); +} +``` + +#### Step 3: Implementation Complete + +The system now automatically: +- Saves/loads weather-specific settings to JSON +- Interpolates variables during weather transitions +- Appears in the weather editor UI +- Handles default values and missing dataanced Usage + +### Custom Variable Types + +Create custom weather variable types for complex data: + +```cpp +class CustomTypeVariable : public WeatherVariables::WeatherVariable +{ +public: + CustomTypeVariable(const std::string& name, MyCustomType* valuePtr, MyCustomType defaultValue) : + WeatherVariable(name, name, "", valuePtr, defaultValue, + [](const MyCustomType& from, const MyCustomType& to, float factor) { + // Custom interpolation logic + MyCustomType result; + result.field1 = std::lerp(from.field1, to.field1, factor); + result.field2 = from.field2; // No lerp for this field + return result; + }) + { + } +}; +``` + +## System Integration + +### How Weather Manager Uses the Registry + +The weather manager detects and updates features automatically: + +```cpp +// Detection +if (globalRegistry->HasWeatherSupport(featureName)) { + // Feature has registered variables +} + +// Loading +json currWeatherSettings, nextWeatherSettings; +LoadSettingsFromWeather(weather, featureName, currWeatherSettings); +LoadSettingsFromWeather(lastWeather, featureName, nextWeatherSettings); + +// Interpolation during weather transitions +globalRegistry->UpdateFeatureFromWeathers( + featureName, + currWeatherSettings, + nextWeatherSettings, + lerpFactor // 0.0 to 1.0 +); +``` + +### File Structure + +Weather-specific settings are stored in: +``` +Data/SKSE/Plugins/CommunityShaders/Weathers/ + WeatherEditorID_FormID.json +``` + +Each file contains settings for all features: +```json +{ + "FeatureName1": { + "Intensity": 1.5, + "Color": [1.0, 0.8, 0.6] + }, + "FeatureName2": { + "Enabled": true + } +} +``` + +## Advanced Patterns + +### Conditional Registration + +Register variables based on feature state: + +```cpp +void RegisterWeatherVariables() override +{ + auto* registry = WeatherVariables::GlobalWeatherRegistry::GetSingleton() + ->GetOrCreateFeatureRegistry(GetShortName()); + + // Core variables + registry->RegisterVariable(std::make_shared( + "intensity", "Intensity", "Main intensity", + &settings.intensity, 1.0f, 0.0f, 2.0f + )); + + // Advanced variables (conditional) + if (settings.enableAdvancedMode) { + registry->RegisterVariable(std::make_shared( + "advancedColor", "Advanced Color", "Color tuning", + &settings.advancedColor, float3{ 1.0f, 1.0f, 1.0f } + )); + } +} +``` + +### Runtime Queries + +Access registered variables for debugging or dynamic UI: + +```cpp +auto* registry = WeatherVariables::GlobalWeatherRegistry::GetSingleton() + ->GetFeatureRegistry("MyFeature"); + +if (registry) { + for (const auto& var : registry->GetVariables()) { + logger::info("{}: {}", var->GetDisplayName(), var->GetName()); + } +} +``` + +## Implementation Notes + +### Memory Management +- Registry uses `std::shared_ptr` for variable lifetime +- Variables store raw pointers to feature data (safe as features outlive registry) +- No copying - variables are modified in-place + +### Thread Safety +Current implementation is single-threaded (main game thread). Variables are accessed and modified on the same thread that updates weather. + +### JSON Serialization +Uses nlohmann::json for type conversion. Built-in support for: +- Primitive types (float, int, bool) +- float2, float3, float4 (see `Utils/Serialize.h`) +- Custom types require NLOHMANN_DEFINE_TYPE_* macros + +### Error Handling +- Missing JSON keys use default values +- Type mismatches caught by json exceptions +- Invalid weather files logged but don't crashherVariables::FloatVariable>( + "intensity", "Intensity", "Effect intensity", + &settings.intensity, 1.0f, 0.0f, 2.0f + )); + } + + // That's it! No save/load/update code needed for weather variables +}; +``` + +## Architecture Benefits + +### Separation of Concerns +- **Features**: Focus on rendering logic and effect implementation +- **Weather System**: Handles persistence, interpolation, and state management +- **UI Layer**: Automatically discovers registered variables for editor display + +### Future Enhancements +The centralized registry enables: +- Weather template inheritance (parent weather settings override children) +- Automatic UI generation for weather variable editing +- Bulk operations (reset all weathers to defaults, copy settings, etc.) +- Variable validation and constraints +- Change tracking and undo/redo support + +### Performance +- Variables are directly modified in place (no copying) +- Interpolation only happens during weather transitions +- Registration is one-time during feature initialization diff --git a/docs/weather-system-docs/examples/ExampleWeatherFeature.cpp b/docs/weather-system-docs/examples/ExampleWeatherFeature.cpp new file mode 100644 index 0000000000..b9a251c23e --- /dev/null +++ b/docs/weather-system-docs/examples/ExampleWeatherFeature.cpp @@ -0,0 +1,115 @@ +// Example Feature: Demonstrates the Weather Variable Registration Pattern +// +// This is a minimal example showing how features can support per-weather settings. +// Simply register your variables once during RegisterWeatherVariables() and the weather system handles the rest. + +#include "ExampleWeatherFeature.h" +#include "WeatherVariableRegistry.h" + +void ExampleWeatherFeature::RegisterWeatherVariables() +{ + // Get or create the registry for this feature + auto* registry = WeatherVariables::GlobalWeatherRegistry::GetSingleton() + ->GetOrCreateFeatureRegistry(GetShortName()); + + // Register a simple float variable + registry->RegisterVariable(std::make_shared( + "Intensity", // JSON key name + "Effect Intensity", // Display name for UI + "Controls how strong the effect is", // Tooltip/description + &settings.intensity, // Pointer to the actual variable + 1.0f, // Default value + 0.0f, 2.0f // Min/max range (optional) + )); + + // Register a float3 color variable + registry->RegisterVariable(std::make_shared( + "Color", + "Effect Color", + "RGB color values for the effect", + &settings.color, + float3{ 1.0f, 0.5f, 0.2f } // Default orange color + )); + + // Register a float4 variable (could be RGBA color or other 4-component data) + registry->RegisterVariable(std::make_shared( + "TintColor", + "Tint Color", + "RGBA tint color with alpha", + &settings.tintColor, + float4{ 1.0f, 1.0f, 1.0f, 1.0f } // Default white with full opacity + )); + + // Register a custom type with manual lerp function + // This example shows a bool that transitions at the halfway point + registry->RegisterVariable(std::make_shared>( + "Enabled", + "Enable Effect", + "Toggle the effect on/off", + &settings.enabled, + true, // Default enabled + [](const bool& from, const bool& to, float factor) { + // Custom lerp: switch to target value when more than halfway through transition + return factor > 0.5f ? to : from; + } + )); + + // That's it! No need to override: + // - SupportsWeather() - automatically detected via HasWeatherSupport() + // - UpdateSettingsFromWeathers() - handled by registry + // - SaveSettings() / LoadSettings() for weather data - automatic +} + +void ExampleWeatherFeature::DrawSettings() +{ + ImGui::TextWrapped("This is an example feature demonstrating the weather variable registration system."); + ImGui::Spacing(); + + // Draw regular feature settings UI + if (ImGui::SliderFloat("Intensity", &settings.intensity, 0.0f, 2.0f)) { + // Settings changed - if you want to save immediately, call: + // State::GetSingleton()->Save(); + } + + if (ImGui::ColorEdit3("Color", &settings.color.x)) { + // Color changed + } + + if (ImGui::ColorEdit4("Tint Color", &settings.tintColor.x)) { + // Tint changed + } + + if (ImGui::Checkbox("Enabled", &settings.enabled)) { + // Enabled state changed + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::TextWrapped("Note: When weather-specific settings exist for the current weather, " + "they will automatically override these base settings. The weather system " + "handles all interpolation during weather transitions."); +} + +void ExampleWeatherFeature::LoadSettings(json& o_json) +{ + // Load base settings (non-weather-specific) + if (o_json.contains("Intensity")) + settings.intensity = o_json["Intensity"]; + if (o_json.contains("Color")) + settings.color = o_json["Color"]; + if (o_json.contains("TintColor")) + settings.tintColor = o_json["TintColor"]; + if (o_json.contains("Enabled")) + settings.enabled = o_json["Enabled"]; +} + +void ExampleWeatherFeature::SaveSettings(json& o_json) +{ + // Save base settings (non-weather-specific) + o_json["Intensity"] = settings.intensity; + o_json["Color"] = settings.color; + o_json["TintColor"] = settings.tintColor; + o_json["Enabled"] = settings.enabled; +} diff --git a/docs/weather-system-docs/examples/ExampleWeatherFeature.h b/docs/weather-system-docs/examples/ExampleWeatherFeature.h new file mode 100644 index 0000000000..ee21dd765d --- /dev/null +++ b/docs/weather-system-docs/examples/ExampleWeatherFeature.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Feature.h" + +// Example feature demonstrating the weather variable registration pattern +class ExampleWeatherFeature : public Feature +{ +public: + // Feature settings structure + struct Settings + { + float intensity = 1.0f; + float3 color = { 1.0f, 0.5f, 0.2f }; + float4 tintColor = { 1.0f, 1.0f, 1.0f, 1.0f }; + bool enabled = true; + } settings; + + // Feature interface + std::string GetName() override { return "Example Weather Feature"; } + std::string GetShortName() override { return "ExampleWeather"; } + + // Register weather-controllable variables + // This is the ONLY weather-related method you need to implement! + void RegisterWeatherVariables() override; + + // Standard feature methods + void DrawSettings() override; + void LoadSettings(json& o_json) override; + void SaveSettings(json& o_json) override; +}; diff --git a/features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl b/features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl new file mode 100644 index 0000000000..82de8d246c --- /dev/null +++ b/features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl @@ -0,0 +1,45 @@ +#include "Common/Color.hlsli" +#include "Common/Math.hlsli" +#include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" + +TextureCube ReflectionTexture : register(t0); +RWTexture2D IBLTexture : register(u0); + +SamplerState LinearSampler : register(s0); + +[numthreads(1, 1, 1)] void main(uint3 dispatchID + : SV_DispatchThreadID) { + // Initialise sh to 0 + sh2 shR = SphericalHarmonics::Zero(); + sh2 shG = SphericalHarmonics::Zero(); + sh2 shB = SphericalHarmonics::Zero(); + + float axisSampleCount = 32; + + // Accumulate coefficients according to surounding direction/color tuples. + for (float az = 0.5; az < axisSampleCount; az += 1.0) + for (float ze = 0.5; ze < axisSampleCount; ze += 1.0) { + float3 rayDir = SphericalHarmonics::GetUniformSphereSample(az / axisSampleCount, ze / axisSampleCount); + + float3 color = ReflectionTexture.SampleLevel(LinearSampler, -rayDir, 0); + + color = Color::GammaToLinear(color); + + sh2 sh = SphericalHarmonics::Evaluate(rayDir); + + shR = SphericalHarmonics::Add(shR, SphericalHarmonics::Scale(sh, color.r)); + shG = SphericalHarmonics::Add(shG, SphericalHarmonics::Scale(sh, color.g)); + shB = SphericalHarmonics::Add(shB, SphericalHarmonics::Scale(sh, color.b)); + } + + // Integrating over a sphere so each sample has a weight of 4*PI/samplecount (uniform solid angle, for each sample) + float shFactor = 4.0 * Math::PI / (axisSampleCount * axisSampleCount); + + shR = SphericalHarmonics::Scale(shR, shFactor); + shG = SphericalHarmonics::Scale(shG, shFactor); + shB = SphericalHarmonics::Scale(shB, shFactor); + + IBLTexture[int2(0, 0)] = shR; + IBLTexture[int2(1, 0)] = shG; + IBLTexture[int2(2, 0)] = shB; +} diff --git a/src/Feature.h b/src/Feature.h index 0a9d8e3671..45d6463267 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -129,20 +129,11 @@ struct Feature virtual WeatherAnalysisConfig GetWeatherAnalysisConfig() const { return {}; } /** - * @brief Indicates whether this feature supports per-weather settings - * @return True if the feature has weather-specific settings that should be interpolated + * @brief Called during feature initialization to register weather-controllable variables + * Features should register their weather variables here using the WeatherVariables::GlobalWeatherRegistry + * The weather system will automatically handle save/load/lerp for all registered variables */ - virtual bool SupportsWeather() const { return false; } - - /** - * @brief Updates feature settings based on weather transition - * Called by WeatherManager when weather changes or transitions occur. - * Features should lerp between currWeather and nextWeather settings using lerpFactor. - * @param currWeather Settings from the current weather (may be empty if no settings exist) - * @param nextWeather Settings from the transitioning weather (may be empty) - * @param lerpFactor Blend factor between weathers (0.0 = fully nextWeather, 1.0 = fully currWeather) - */ - virtual void UpdateSettingsFromWeathers(const json& currWeather, const json& nextWeather, float lerpFactor) {} + virtual void RegisterWeatherVariables() {} virtual bool ValidateCache(CSimpleIniA& a_ini); virtual void WriteDiskCacheInfo(CSimpleIniA& a_ini); diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp index 72fd300082..c96cea5095 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/WeatherEditor.cpp @@ -1,11 +1,11 @@ -#include "Weather.h" +#include "WeatherEditor.h" #include "Deferred.h" #include "State.h" #include "Util.h" #include "WeatherManager.h" -#include "Editor/EditorWindow.h" +#include "WeatherEditor/EditorWindow.h" int8_t LerpInt8_t(const int8_t oldValue, const int8_t newVal, const float lerpValue) { diff --git a/src/State.cpp b/src/State.cpp index dfc514af1c..8105988067 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -350,6 +350,9 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) // Load base feature settings from merged config (default + user) feature->Load(settings); + // Register weather variables (features opt-in by implementing this) + feature->RegisterWeatherVariables(); + // Apply new/changed feature-specific overrides if any if (overridesDiscovered > 0) { json featureJson; diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 65c69951d2..276088cce2 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1,10 +1,10 @@ #include "EditorWindow.h" #include "State.h" -#include "features/Weather.h" +#include "Features/WeatherEditor.h" #include "imgui_internal.h" -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(EditorWindow::Settings, recordMarkers, markedRecords) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges) bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring) { @@ -164,8 +164,10 @@ void EditorWindow::ShowObjectsWindow() // Status column ImGui::TableNextColumn(); + // Re-check if the record exists after potential removal + markedRecord = settings.markedRecords.find(editorLabel); if (markedRecord != settings.markedRecords.end()) { - ImGui::Text(markedRecord->second.c_str()); + ImGui::Text("%s", markedRecord->second.c_str()); } } @@ -240,6 +242,9 @@ void EditorWindow::RenderUI() context->ClearRenderTargetView(framebuffer.RTV, (float*)&ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); + // Increase background opacity for all editor windows + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); + // Check for Escape key to close editor if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { open = false; @@ -260,10 +265,19 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Close All Lighting Widgets")) { for (auto* widget : lightingTemplateWidgets) widget->SetOpen(false); } - ImGui::Separator(); - if (ImGui::MenuItem("Editor Settings")) { + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Settings")) { + if (ImGui::MenuItem("Editor Preferences")) { showSettingsWindow = !showSettingsWindow; } + ImGui::Separator(); + if (ImGui::Checkbox("Auto-Apply Changes", &settings.autoApplyChanges)) { + Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Automatically apply weather changes to the game as you edit"); + } ImGui::EndMenu(); } if (ImGui::BeginMenu("Window")) { @@ -337,7 +351,7 @@ void EditorWindow::RenderUI() // Close button on the right side float menuBarHeight = ImGui::GetFrameHeight(); - ImGui::SameLine(ImGui::GetWindowWidth() - menuBarHeight); + ImGui::SameLine(ImGui::GetWindowWidth() - menuBarHeight - 10.0f); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); @@ -371,6 +385,9 @@ void EditorWindow::RenderUI() } ShowWidgetWindow(); + + // Pop the alpha style var + ImGui::PopStyleVar(); } void EditorWindow::SetupResources() @@ -405,6 +422,27 @@ void EditorWindow::SetupResources() void EditorWindow::Draw() { + // Track editor open state for vanity camera management + static bool wasOpen = false; + + if (open && !wasOpen) { + // Editor just opened - disable vanity camera + DisableVanityCamera(); + } else if (!open && wasOpen) { + // Editor just closed - restore vanity camera + RestoreVanityCamera(); + } + + wasOpen = open; + + // Re-enforce weather lock if active (handles time changes) + if (weatherLockActive && lockedWeather) { + auto sky = RE::Sky::GetSingleton(); + if (sky && sky->currentWeather != lockedWeather) { + sky->ForceWeather(lockedWeather, false); + } + } + auto renderer = RE::BSGraphics::Renderer::GetSingleton(); auto& framebuffer = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; @@ -462,7 +500,7 @@ void EditorWindow::LoadSettings() void EditorWindow::ShowSettingsWindow() { - ImGui::Begin("Settings"); + ImGui::Begin("Settings", &showSettingsWindow); // Static variable to track the selected category static std::string selectedOption = "Preferences"; @@ -637,3 +675,28 @@ void EditorWindow::ResumeTime() logger::info("Time resumed (timescale: {})", savedTimeScale); } } + +void EditorWindow::DisableVanityCamera() +{ + if (vanityCameraDisabled) return; + + auto setting = RE::GetINISetting("fAutoVanityModeDelay:Camera"); + if (setting) { + savedVanityCameraDelay = setting->GetFloat(); + setting->data.f = 10000.0f; + vanityCameraDisabled = true; + logger::info("Vanity camera disabled (saved delay: {})", savedVanityCameraDelay); + } +} + +void EditorWindow::RestoreVanityCamera() +{ + if (!vanityCameraDisabled) return; + + auto setting = RE::GetINISetting("fAutoVanityModeDelay:Camera"); + if (setting) { + setting->data.f = savedVanityCameraDelay; + vanityCameraDisabled = false; + logger::info("Vanity camera restored (delay: {})", savedVanityCameraDelay); + } +} diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index f4e2022fb6..2c17f5fc73 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -34,6 +34,10 @@ class EditorWindow bool timePaused = false; float savedTimeScale = 1.0f; + // Vanity camera control + bool vanityCameraDisabled = false; + float savedVanityCameraDelay = 180.0f; + void ShowObjectsWindow(); void ShowViewportWindow(); @@ -55,6 +59,9 @@ class EditorWindow void ResumeTime(); bool IsTimePaused() const { return timePaused; } + void DisableVanityCamera(); + void RestoreVanityCamera(); + struct Settings { std::map recordMarkers = { @@ -63,6 +70,7 @@ class EditorWindow { "Done", { 0.05f, 0.85f, 0.3f, 1 } } }; std::map markedRecords; + bool autoApplyChanges = true; }; Settings settings; diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index a1985ccc08..641265f5ea 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -1,13 +1,9 @@ #include "WeatherWidget.h" #include "../EditorWindow.h" - -#include - -#include "Feature.h" #include "State.h" -#include "Util.h" #include "WeatherManager.h" +#include "WeatherVariableRegistry.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Atmosphere, colorTimes) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DirectionalColor, max, min) @@ -39,9 +35,6 @@ void WeatherWidget::DrawWidget() // Weather lock controls if (isLocked) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); - ImGui::TextWrapped("WEATHER LOCKED - This weather is active and won't change with time"); - ImGui::PopStyleColor(); if (ImGui::Button("Unlock Weather", ImVec2(-1, 0))) { editorWindow->UnlockWeather(); } @@ -53,39 +46,79 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Force this weather to be active and prevent time-based weather changes"); } } - ImGui::Separator(); - // Auto-apply toggle and manual buttons - ImGui::Checkbox("Auto-Apply Changes", &autoApply); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Automatically apply changes to the game as you edit"); - } - ImGui::SameLine(); - // Time pause toggle bool isPaused = editorWindow->IsTimePaused(); if (isPaused) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); - if (ImGui::Button("Resume Time", ImVec2(ImGui::GetContentRegionAvail().x, 0))) { + if (ImGui::Button("Resume Time", ImVec2(-1, 0))) { editorWindow->ResumeTime(); } ImGui::PopStyleColor(2); } else { - if (ImGui::Button("Pause Time", ImVec2(ImGui::GetContentRegionAvail().x, 0))) { + if (ImGui::Button("Pause Time", ImVec2(-1, 0))) { editorWindow->PauseTime(); } } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Pause/resume time progression in game"); } - - if (ImGui::Button("Apply Now", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { - ApplyChanges(); - } - ImGui::SameLine(); - if (ImGui::Button("Revert", ImVec2(-1, 0))) { - RevertChanges(); + + // Show Apply/Revert buttons only when auto-apply is disabled + if (!editorWindow->settings.autoApplyChanges) { + auto menu = globals::menu; + bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && + menu->uiIcons.saveSettings.texture && + menu->uiIcons.featureSettingRevert.texture; + + if (useIcons) { + // Icon-based buttons + const float iconSize = ImGui::GetFrameHeight(); + const ImVec2 buttonSize(iconSize, iconSize); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + + if (ImGui::ImageButton("##ApplyWeather", menu->uiIcons.saveSettings.texture, buttonSize)) { + ApplyChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + + ImGui::SameLine(); + + if (ImGui::ImageButton("##RevertWeather", menu->uiIcons.featureSettingRevert.texture, buttonSize)) { + RevertChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } + + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + } else { + // Text-based fallback buttons + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); + if (ImGui::Button("Apply", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { + ApplyChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + if (ImGui::Button("Revert", ImVec2(-1, 0))) { + RevertChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } + } } ImGui::Separator(); @@ -414,7 +447,7 @@ void WeatherWidget::DrawDALCSettings() } } } - if (changed && autoApply) { + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } } @@ -446,7 +479,7 @@ void WeatherWidget::DrawWeatherColorSettings() } } - if (changed && autoApply) { + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } } @@ -486,7 +519,7 @@ void WeatherWidget::DrawCloudSettings() } } } - if (changed && autoApply) { + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } } @@ -526,7 +559,7 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.autoApplyChanges) { ApplyChanges(); } } @@ -549,6 +582,7 @@ void WeatherWidget::InheritFromParent(const std::string& property) void WeatherWidget::SaveFeatureSettings() { auto* weatherManager = WeatherManager::GetSingleton(); + auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); for (const auto& [featureName, featureJson] : settings.featureSettings) { if (!featureJson.empty()) { @@ -560,15 +594,23 @@ void WeatherWidget::SaveFeatureSettings() void WeatherWidget::LoadFeatureSettings() { auto* weatherManager = WeatherManager::GetSingleton(); + auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); for (auto* feature : Feature::GetFeatureList()) { - if (!feature || !feature->loaded || !feature->SupportsWeather()) { + if (!feature || !feature->loaded) { + continue; + } + + std::string featureName = feature->GetShortName(); + + // Check if feature has registered weather variables + if (!globalRegistry->HasWeatherSupport(featureName)) { continue; } json featureJson; - if (weatherManager->LoadSettingsFromWeather(weather, feature->GetShortName(), featureJson)) { - settings.featureSettings[feature->GetShortName()] = featureJson; + if (weatherManager->LoadSettingsFromWeather(weather, featureName, featureJson)) { + settings.featureSettings[featureName] = featureJson; } } } @@ -591,12 +633,20 @@ void WeatherWidget::DrawFeatureSettings() ImGui::TextWrapped("Configure feature-specific settings that will be applied when this weather is active."); ImGui::Spacing(); + auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); + for (auto* feature : Feature::GetFeatureList()) { - if (!feature || !feature->loaded || !feature->SupportsWeather()) { + if (!feature || !feature->loaded) { continue; } std::string featureName = feature->GetShortName(); + + // Check if feature has registered weather variables + if (!globalRegistry->HasWeatherSupport(featureName)) { + continue; + } + std::string displayName = feature->GetName(); if (ImGui::TreeNode(displayName.c_str())) { diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h index d33a4a814a..d18611169f 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.h +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -63,7 +63,6 @@ class WeatherWidget : public Widget }; Settings settings; - bool autoApply = true; // Auto-apply changes to game in real-time ~WeatherWidget(); diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp index c6304f079e..fbe4469222 100644 --- a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp @@ -1,5 +1,4 @@ #include "WorldSpaceWidget.h" -#include "Util.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(WorldSpaceWidget::Settings, temp) diff --git a/src/WeatherManager.cpp b/src/WeatherManager.cpp index 74c48ecf04..c7b54ab2c0 100644 --- a/src/WeatherManager.cpp +++ b/src/WeatherManager.cpp @@ -73,29 +73,33 @@ void WeatherManager::UpdateFeatures() // Always update if lerp factor changes or weather changed if (weatherChanged || std::abs(currentWeathers.lerpFactor - lastKnownWeather.lerpFactor) > 0.001f) { - // Get all features and update those that support weather + auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); + + // Get all features and update those that have registered weather variables for (auto* feature : Feature::GetFeatureList()) { if (!feature || !feature->loaded) { continue; } - // Check if feature supports weather (will be added to Feature class) - if (feature->SupportsWeather()) { + std::string featureName = feature->GetShortName(); + + // Check if feature has registered weather variables + if (globalRegistry->HasWeatherSupport(featureName)) { json currWeatherSettings; json nextWeatherSettings; // Load settings for current weather if (currentWeathers.currentWeather) { - LoadSettingsFromWeather(currentWeathers.currentWeather, feature->GetShortName(), currWeatherSettings); + LoadSettingsFromWeather(currentWeathers.currentWeather, featureName, currWeatherSettings); } // Load settings for transitioning weather if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { - LoadSettingsFromWeather(currentWeathers.lastWeather, feature->GetShortName(), nextWeatherSettings); + LoadSettingsFromWeather(currentWeathers.lastWeather, featureName, nextWeatherSettings); } - // Let the feature update its settings based on weather transition - feature->UpdateSettingsFromWeathers(currWeatherSettings, nextWeatherSettings, currentWeathers.lerpFactor); + // Let the global registry handle variable interpolation + globalRegistry->UpdateFeatureFromWeathers(featureName, currWeatherSettings, nextWeatherSettings, currentWeathers.lerpFactor); } } diff --git a/src/WeatherManager.h b/src/WeatherManager.h index 06305d4610..3b9fbe8f9c 100644 --- a/src/WeatherManager.h +++ b/src/WeatherManager.h @@ -1,5 +1,6 @@ #pragma once +#include "WeatherVariableRegistry.h" #include #include diff --git a/src/WeatherVariableRegistry.h b/src/WeatherVariableRegistry.h new file mode 100644 index 0000000000..662fdc3ffb --- /dev/null +++ b/src/WeatherVariableRegistry.h @@ -0,0 +1,256 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +// Weather variable system - features register variables which are automatically handled by weather system +namespace WeatherVariables +{ + // Base class for weather-controllable variables + class IWeatherVariable + { + public: + virtual ~IWeatherVariable() = default; + virtual void Lerp(const json& from, const json& to, float factor) = 0; + virtual void SaveToJson(json& j) const = 0; + virtual void LoadFromJson(const json& j) = 0; + virtual void SetToDefault() = 0; + virtual std::string GetName() const = 0; + virtual std::string GetDisplayName() const = 0; + virtual std::string GetTooltip() const = 0; + }; + + // Templated weather variable for type safety + template + class WeatherVariable : public IWeatherVariable + { + public: + WeatherVariable(const std::string& name, const std::string& displayName, const std::string& tooltip, + T* valuePtr, T defaultValue, + std::function lerpFunc = nullptr) : + name(name), + displayName(displayName), tooltip(tooltip), valuePtr(valuePtr), defaultValue(defaultValue), lerpFunc(lerpFunc) + { + if (!lerpFunc) { + // Default lerp for float types + if constexpr (std::is_floating_point_v) { + this->lerpFunc = [](const T& from, const T& to, float factor) { + return static_cast(std::lerp(from, to, factor)); + }; + } + } + } + + void Lerp(const json& from, const json& to, float factor) override + { + if (!valuePtr || !lerpFunc) + return; + + T fromVal = from.is_null() ? defaultValue : from.get(); + T toVal = to.is_null() ? defaultValue : to.get(); + *valuePtr = lerpFunc(fromVal, toVal, factor); + } + + void SaveToJson(json& j) const override + { + if (valuePtr) { + j[name] = *valuePtr; + } + } + + void LoadFromJson(const json& j) override + { + if (valuePtr && j.contains(name)) { + *valuePtr = j[name].get(); + } + } + + void SetToDefault() override + { + if (valuePtr) { + *valuePtr = defaultValue; + } + } + + std::string GetName() const override { return name; } + std::string GetDisplayName() const override { return displayName; } + std::string GetTooltip() const override { return tooltip; } + + private: + std::string name; + std::string displayName; + std::string tooltip; + T* valuePtr; + T defaultValue; + std::function lerpFunc; + }; + + // Specialized weather variables for common types + class FloatVariable : public WeatherVariable + { + public: + FloatVariable(const std::string& name, const std::string& displayName, const std::string& tooltip, + float* valuePtr, float defaultValue, float minValue = 0.0f, float maxValue = 1.0f) : + WeatherVariable(name, displayName, tooltip, valuePtr, defaultValue), + minValue(minValue), maxValue(maxValue) + { + } + + float GetMin() const { return minValue; } + float GetMax() const { return maxValue; } + + private: + float minValue; + float maxValue; + }; + + class Float3Variable : public WeatherVariable + { + public: + Float3Variable(const std::string& name, const std::string& displayName, const std::string& tooltip, + float3* valuePtr, float3 defaultValue) : + WeatherVariable(name, displayName, tooltip, valuePtr, defaultValue, + [](const float3& from, const float3& to, float factor) { + return float3{ + std::lerp(from.x, to.x, factor), + std::lerp(from.y, to.y, factor), + std::lerp(from.z, to.z, factor) + }; + }) + { + } + }; + + class Float4Variable : public WeatherVariable + { + public: + Float4Variable(const std::string& name, const std::string& displayName, const std::string& tooltip, + float4* valuePtr, float4 defaultValue) : + WeatherVariable(name, displayName, tooltip, valuePtr, defaultValue, + [](const float4& from, const float4& to, float factor) { + return float4{ + std::lerp(from.x, to.x, factor), + std::lerp(from.y, to.y, factor), + std::lerp(from.z, to.z, factor), + std::lerp(from.w, to.w, factor) + }; + }) + { + } + }; + + // Registry for a feature's weather variables + class FeatureWeatherRegistry + { + public: + template + void RegisterVariable(std::shared_ptr> var) + { + variables.push_back(var); + } + + void LerpAllVariables(const json& from, const json& to, float factor) + { + for (auto& var : variables) { + json fromVar = from.is_null() || !from.contains(var->GetName()) ? json{} : from[var->GetName()]; + json toVar = to.is_null() || !to.contains(var->GetName()) ? json{} : to[var->GetName()]; + var->Lerp(fromVar, toVar, factor); + } + } + + void SaveAllToJson(json& j) const + { + for (const auto& var : variables) { + var->SaveToJson(j); + } + } + + void LoadAllFromJson(const json& j) + { + for (auto& var : variables) { + var->LoadFromJson(j); + } + } + + void SetAllToDefaults() + { + for (auto& var : variables) { + var->SetToDefault(); + } + } + + const std::vector>& GetVariables() const { return variables; } + + private: + std::vector> variables; + }; + + // Global registry mapping feature names to their weather variables + class GlobalWeatherRegistry + { + public: + static GlobalWeatherRegistry* GetSingleton() + { + static GlobalWeatherRegistry singleton; + return &singleton; + } + + FeatureWeatherRegistry* GetOrCreateFeatureRegistry(const std::string& featureName) + { + auto it = featureRegistries.find(featureName); + if (it == featureRegistries.end()) { + featureRegistries[featureName] = std::make_unique(); + } + return featureRegistries[featureName].get(); + } + + FeatureWeatherRegistry* GetFeatureRegistry(const std::string& featureName) + { + auto it = featureRegistries.find(featureName); + return it != featureRegistries.end() ? it->second.get() : nullptr; + } + + bool HasWeatherSupport(const std::string& featureName) const + { + return featureRegistries.find(featureName) != featureRegistries.end(); + } + + void UpdateFeatureFromWeathers(const std::string& featureName, const json& currWeather, const json& nextWeather, float lerpFactor) + { + auto* registry = GetFeatureRegistry(featureName); + if (registry) { + registry->LerpAllVariables(currWeather, nextWeather, lerpFactor); + } + } + + void SaveFeatureToJson(const std::string& featureName, json& j) const + { + auto it = featureRegistries.find(featureName); + if (it != featureRegistries.end()) { + it->second->SaveAllToJson(j); + } + } + + void LoadFeatureFromJson(const std::string& featureName, const json& j) + { + auto* registry = GetFeatureRegistry(featureName); + if (registry) { + registry->LoadAllFromJson(j); + } + } + + private: + GlobalWeatherRegistry() = default; + ~GlobalWeatherRegistry() = default; + GlobalWeatherRegistry(const GlobalWeatherRegistry&) = delete; + GlobalWeatherRegistry& operator=(const GlobalWeatherRegistry&) = delete; + + std::map> featureRegistries; + }; +} From 69381eadd63681bcb2682d550101a41e9dc1726d Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 8 Dec 2025 19:36:56 +1000 Subject: [PATCH 03/30] moar fix --- .../Icons/Action Icons/apply-to-game.png | Bin 0 -> 3214 bytes .../Icons/Action Icons/delete.png | Bin 0 -> 2465 bytes .../CommunityShaders/Icons/Categories/sky.png | Bin 3630 -> 2044 bytes src/Features/CloudShadows.h | 2 +- src/Features/SkySync.h | 2 +- src/Features/WeatherEditor.cpp | 133 +--- src/Features/WeatherEditor.h | 10 +- src/Features/WeatherPicker.cpp | 13 + src/Features/WeatherPicker.h | 2 +- src/Menu.cpp | 3 +- src/Menu.h | 2 + src/Menu/FeatureListRenderer.cpp | 2 +- src/Utils/UI.cpp | 8 +- src/WeatherEditor/EditorWindow.cpp | 391 ++++++++-- src/WeatherEditor/EditorWindow.h | 25 +- .../Weather/ImageSpaceWidget.cpp | 310 ++++++++ src/WeatherEditor/Weather/ImageSpaceWidget.h | 60 ++ .../Weather/LightingTemplateWidget.cpp | 278 ++++++- .../Weather/LightingTemplateWidget.h | 45 ++ src/WeatherEditor/Weather/WeatherWidget.cpp | 733 +++++++++++++----- src/WeatherEditor/Weather/WeatherWidget.h | 33 + .../Weather/WorldSpaceWidget.cpp | 75 +- src/WeatherEditor/Weather/WorldSpaceWidget.h | 4 +- src/WeatherEditor/WeatherUtils.h | 2 + src/WeatherEditor/Widget.cpp | 134 +++- src/WeatherEditor/Widget.h | 1 + 26 files changed, 1885 insertions(+), 383 deletions(-) create mode 100644 package/Interface/CommunityShaders/Icons/Action Icons/apply-to-game.png create mode 100644 package/Interface/CommunityShaders/Icons/Action Icons/delete.png create mode 100644 src/WeatherEditor/Weather/ImageSpaceWidget.cpp create mode 100644 src/WeatherEditor/Weather/ImageSpaceWidget.h diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/apply-to-game.png b/package/Interface/CommunityShaders/Icons/Action Icons/apply-to-game.png new file mode 100644 index 0000000000000000000000000000000000000000..7df06026e59679db5fb044c954391964148c1ec7 GIT binary patch literal 3214 zcmV;93~}>`P)@~0drDELIAGL9O(c600d`2O+f$vv5yP9kH_Qjcsw4D$K&yMJRXmy z{{+x;5HX~eZ93A|yE%XImX5hPq96z&@N^3j`cpbC=y->Actyu!@U$E5?MhfuTwyYz z^oCwd0W2U$hI&aNI(BJlm_rX3Ku;jyk3nAepnl^i^n~7w06g^5%MQ4Kjl+A;1A3tV zBzzY9E6vBLMgMy3-@4&@8$Mn!q_0`*krG^jr+G*LraGeyYJ$mXB8%tXX$Q(W&!YCy z4U;8h7QDpV22Zn)5JspcgqpCBQSVRizJaD8A)IN1a{-n>{WM_hH#`-<#PNgDy_3#0 zH2jXA2AYBzf2khAQC~v^=v;^NqVvK< zkho|g5C9X4pGO#-<$p}SJ%MWH4k3wU@VR5xb?1{&Fj=NuXnM>$_1T%~Sv<$?7|t~` zoGV4#B4-86xdy(Uq2cejO-AaTrO_Ug7@fJ z3C^Gkpx!$t0neWyGz7X=fa zRRz!mW!QD;JUQdGpr+S}=3{q(GBkUquDl&!AI#6SsFV(0IUJ1Ua3xO#duQ|AgB17;0c8C5$d~MlU)eq}C8sl`mf6jOKVN`vhrPnEi0zb~?lS zjWz6Z*@v3gQUJCC_+qt23OKB;X`|=g#N|k!+LP_8Q1g`tEVZZ2q|gw|8hb()EZ%^R zPRUKJPfm?NZb0N76gMQq_5m4)L5?$J5Wv`lO^{>sMPPBOnAN?3$_d0u&{%QBM=yD; zoGnNeJPVO}@=uz-eXv4Wz33HT{__IvT|SW;$y7%&rPA$I-C3Q5)mp*o!c4tfG(j}( z{d(cQ{{I4Yb;4)(v(M-|)GaPGU)lcsmUx}uJd59|yWM)RXu7}|z36h*hbS1fB!^C> z(Z@Z1xq$s5J@=fxv-u0`V=~lcooft2sJ0nvKAzQC?&*-K7S2l(U1N(RijMwCL#NKk zgE0Belj3tGD?zY=Osk)#htE&N;vGZ7XSJ4lIz$veS*J72$0Ch)eVu(??#Vy=rM%(x z{nuoxXr=;MUfu^89%$a5NiCx#pBL~(^L_{t475VIgo*_E@V3+l5Nh5RbEHWsfUfb} zr7l4%#0V;~#B3#|m4wti=*}y)>X1}`_Uv8rXz9HIAXdMQf#wl&#odRZMD0^YA5J zWC@LX?Eij&-7+p*ol7}hRvyTdqf4D+E;Ff|J~oa8FBujdVD@r}1^K%zxEM~0c_BHJ ztaB*`dM{^fSn#gX=z8ADUN5JR4UVzwq+yqXi z02h!DN2J!qntB`afSfyd`GN@JDqh7l(>n!VX#Rdq!a4+t(D_bC z_>H87Nn%w2cnDq6n)TmbHi>VjjyH^p)JHa zmj>{-NUE8{{sfU_APr%8yM@*-;8rR^?2sbcB+vJ-2HPXn)56630A-XwaZY3LH*qRf zkEtMdMnw3Gs3WL?5sllV|uwP8s6pQ*L#4o z3J#f=E+|4;hynwyl~nCM*;hfw{*MKhqbkSZan)_&wMGC)cChXj-LCYfDTW&qm#?Y> zOdA!!l8M)^RVg)#^m`m!y9k&@^G&$N)WRaZ-@=MO)^B~1z*awYpA@&yRCM? z61Ei}gzWOM5tJCRs>Tmm%$$UOSVH(AWLI3XUUSiQk*El=OPPdpTokP${@p<^+vFQ{ zSpr>-`HGa^g7BG%?VVK;MWQ%cHS130ud1;N^qX>G2iG}StWoDJd-FbNLezd!v?P|{aduqw#XE46E(MS6)%2ejpjUp zw@?y^_<>arE`NSstRUqlf0|4Ici}@Dy$J&O6m=&GU>lqJ{%j$Uvh9LS)(0RT`WU30 zMdg@uQTR+JM}QP3Uc-FEQnp>tX$4>!R)S6opXs~;Fk}-Pl1lk#ZfE_3m|r-YEcMTM7Cqe5O}*0#j_p z4^+x_FNM$aas)`J>pI33R0or$mm@$*`~av0N7BkaaYL12>9z2gUXK9llu14BYf;E7 zdOZTHqaOP@De%?FZ4o|mYes;S9M^H@UAtwubqc_+>zMBR>Dn@}(V~z|aw`=eg`X9? zf$-n?0;0YfdmfL+@~0drDELIAGL9O(c600d`2O+f$vv5yPcX4kS9fdmL7 zzV~={vB5%Om>FOIqDrMwsZ=VJN~Kb%R4SFi0B|SMi%deMPgL0#rs2=LP-Q>J25<3} zmMW9{PTOEi<$sXrkv4`AZ1rx9OgA$9lK;_aizdOm)k{$t!@sYL;+-zf!W&nC-bFz2K|G%+iqAdeA^?#B4yt4(MWE0>s-(Q*;IFg>|8RImV@Uf}-XWA;By-aFU4MG~mdZDc156AjvpEmLH*Qu0TErnU0hI@bBG>c06`F7~Qhc8%17=%nmnQkc& z;2po?NPcdlkZ$4!34$<@Mi>MVH~Q84DM!v0Y7~4=K3dAOtGyev>YPio=u|L~Fe5aS0q_X) z7?kU5Whe!v;;P3zIehFM+R6HpA_F%wkd6AsJjXBCv~Lh#mtys^ca#vgnSm_S|3pJR zG37c-8G*Glo8`kN|IlzV0}W*W<~e?x(5x5nWBKq6tpIp4kcIk(XP}2;xrWwc@Znec z(BRF$>gpde9Y0f6_zF!j1|NR44-MW7G_*3Z4u4~YfP(M+c?7sZ4-u2BnoZQBANfeD ztABU~dJM`6OSy96{bGSkoYIkcJum|Bjw#nN~Sx%&Qi*j4xoc*ifViJutYt%?iMVDj(WB7+q_cm{e5%667=o9g|;m)y*G zK3L}XHFE`LE^D0<)(-xyYUrCm_65KCwFDv44MhZe$d)Z&voYo7 z3cIlVoJs_7iPj^)+uS?p%BGmJ-Oe^##eKN>=05S2R zssWBj(CC&rHs<-~dIr}zBhPR}Y(WU7!nkwV!WDf22L3}yu>~RMnHOwmhrrKoDmx|+ z_7$@l>`1?~f|wA3GDips*v&uRN6D)|E< z4YzQ)OnQo}!>zy%oy+wDVqtnr4>wld*5(52qk=!F@~fBJdo;Y#4EIzBuxqk?3xMhE zC-|+!3lwsFi~>z~fQ0uZg|f?! zq%T(dPTRmXQhB5jfVSO7gbxvW)f=#45J^m6k5tNg6a#%4c8_=nWN_qrD)2grDDQA_ z)A%7agbeA;M<`|tAA`{Ef1rRs+jq6H*U)mFW%^3m-hG#L{U=p|6WR$>t9lLjqZ;>z zNsm+;JQ<+{z-8!l-PkJqCmTEZ^=y>{F!oz5)ZC&jUuMUmn?pVLLygU=TAv1b=-g0= zfInE_M3qUPM^NWTrG`oZd=R4|%Y&eD0pnA@T`__H{$|TFef0&#!4_8j;C8^0C`PIi z0eS)yIk_B9@k6FS*lZP@sLQ<+a55@ zi2?z=7IAN$`%=Qk1bNt03UA&D>=VgFv8R8EWtM z1DEYlK6DtLQl7i(_(%Z&KjNvqp9AxbUGJe+2GxAS+=(ZFFn1esBc0m&U4iaJ0_g}f zwCtE?)ZXs{=9z`LG_(-#D}LKq@=Xom3qmN{-!acB-9R3wlIMi9mb$YIBO(e$`UABkRUc+vEQ_ z91hP^$rCKQ>~e}?PAIR?D^oGH%gC4N^_LF#ZD%$F_7>x}7X||HG-&j$HhiU0sZ=VJ fN~Kb%R5Ig#NSpk}SS!QT00000NkvXXu0mjfv6QGe literal 0 HcmV?d00001 diff --git a/package/Interface/CommunityShaders/Icons/Categories/sky.png b/package/Interface/CommunityShaders/Icons/Categories/sky.png index 78dda0678977802ae0954db690dc2149ba7ca390..d090791a99dbb0b109ae01d173fe59641d34feaa 100644 GIT binary patch delta 2031 zcmV@~0drDELIAGL9O(c600d`2 zO+f$vv5yP$^4!PufiiUJnEVt;qB6d{BVLI@#*5JCtc zgb+dqA%u{m0Ps?kohpSY?}cm&)#$f)&MP6K`9s!A-g=!08O@pn;AQLeZz0*Fl4RQm5@n96M(C$E8Y12whrfN6u6Q3a0*xp zs=>dvHTJnvfjVF76s5ghXa!SW|C1_rF;*~}Lsz10^d^PO#4ZJ|B{1rm-&(>-`@&rG zu#ZzdNJ3bFDmw1-jga|tRPJ|41l#!c9gSbgh4ZH@hksTG^?Y3qsXtsFw8O4;@xB(L z@n75X>T|V%S=ql2s{GV{mEu{+NbEdcNcYO(t&nh7ec-lh__w|AlVcbjN`Vnmi@l$+ z<2v`C0=Nt~26j9m9A%#%^fW#dza>x=ojV#Kx**^*phUB0<1YYAum6JOxNA2o%yjdt z@rA?(W`F3t$MTeD%)I2O#wsqQh%>WF2oH^J$d^7y(%Au_E`9bWb{{M@zuzhX3 z=YI;O$kpe5${B$PCDS&CYk3}P(43YkImR3uRQajy`$0%Qs)FA;djHuXY+&>mJ$lV? zJC`uiVJ^fC+(wwiJ#H|%NDSvwiNxX>lL7Aee=+3`@na1r2rl5R_~2pO9p9~uR|S@5 z4?39)o=S?k_J`^9Aw$c?QetKKilW2==zp5%IT?D|hP87}N5>mIqsYcf=dK z5EE-qm!W)C*V@Teadi)g1pFrFCIyU1c=SjFGqntHHP`H9tH3=B2cH54HhzhKJA6h6 z!cMjb7E%HSHvTOl=^p5JH*Zr2aJnP7CfITbZevqTi3Rrc;rUKw7rAT+xD7!;Tz|mm zk1y2kWwHOrjuQ;0Y22FRG6VSC%q7m)(*_8^ zs%!+aKqPl?PX$M}G!D4GYxo6p8-XzJhm++p0r-1=GP`@=UmaY|E+iax_SW9EUF*Ha zkUasO5Hm>(xIK{T&uR)Y%(;B)*ME)A-NMbNB_UQ8Vg`RaWiD&P{-^?engY}ID{0IN4B3WNN{G&h;%HSaH9LyY>Io{SD zv@fcBt-Wa-=j*(|gJSAz#pGgk{$_HF)$3Y9=^ev=Ny5O)`CW~DkYmiD{(t+QOw4l> z6;JOg5NPhj^XLDJA^D^Z=-;E*Mo*D?1(iuE;HbCgjn|sYmR$Pt9O?J< z==pY4yOc02KTuEwGbJ!QmqNV}_bk&vG|=Dp#j1W#WH4-Xw2nMS=#SgmY>vJ@&>wS{ zcHLP?_Q5SxJ)l3wQYNVa|1id$KM-_3aHA$o$5K!4XI|csz#VC=$$up^JiWgJ6~58H zA7GPS&O^90e-8Zp{zAxHGRN^sCJ;*iT-tsvWGErvFq`N~331{?L3+Ufdk#=A${9C?D3W1vejenoMJ_Y~qRSN;b!&|^? zEBDpgdZtnI$F^&9)N3vq2gC9(VRfAb!!m1Y?(JuFMO?h|2xf%TzjcS<8B&4;)-LLw zw>Q2MGKZaV`+t@|EFPc%V*^Mpj(r{6)DPzj#aSYBV3Zeay~>0Pp;G0a7&!sX5)q_B z!)UEY^TwSU0RY%c zG&8b=ej``!4Fb^fH+^yw0IaJ3?VZ?8mIra344OLGi*b~q&Y>|O902t8bC_gLKMEUh zltKmRc;r-79TEY0;gOD7mS{^Rf#L(2g$7XULappQL;XB;y^#C$H|lY4kN}OsCL=gB ze>w}t!6VmnanSqfGzy7Wqp74SUni4&Iq8QG<0=!QD_VbgHeMRYOD}Co6J$8v*cDKK4}Q^;y$OfR{5!Ix<-gNuv~R*-v5kYEM0`%_H$MHj0LwmvNkQ3CSd72` zPl|C6h0c~+%fxD4a5f-^;_qYx(kOHmlwCYh1EcdlsPjLe1cpB&zz*706v%ANXf<~@ zB8^O?ID%empRa0PfrAt}^*dzMJO9Ad@*bvxP&}BgYEEASQs=$ zOAWnO4TH7EpmAs{4z0aM1A0NPDX+1i^Wa5hlm8!obyp+wAPY-NoEgYsGXg@^raojBMKerK><*+vHu}a4FLEeL8vs}l>`8k69N;OEDR0AqxYAho6*YI z#zHZ^H^~CFmnF?n*^<8JJ53n+Fxix_xYK0@F4qvg?S6+M3}`313~a;=N(yO*E_3@U zcWzIh>ac3(AEo`)yramZ*xZUSZJ_Snac{)ngRB1&TEtze^};+^M|L0%BYCs%#{sg& zNL+XR;gx~9uIi)?i6TwGbIQ36;^`vkN7Ql^8A9cIF3oYLbBat8g*HA-4ZYsjs+g7c zk@BnL=4f7@>T9c&;rl2D%n>Uml9jc3)%A}9_2DttOBr!yuO+7pm6~-71`cA(PXq8x zP13wG8ONOuDLr*;$hd~6jBQr+^Z6lBksx|5dyg$%MMELf)+AZ0cqL*nH%nlt^HmE% zQ-8B`k#ljy=b=9hL8h5A z3jl<~R&PEaHElZptUpgQGO*{QO$@r^yB?OQpLwdvZ*Ft-4__4QZm8VmY-KZrpUaXJ zC5)wv56N+2Bt7jlU*##rc1QbOA39+;O%M0E&xJYkZ%5xM;e=!xR+sciMT>w4XkTH0;q{V$icreQyy=H?2sHYxcUQky>! zYaQ}PdAUD3Gs`+;B55{^SSPewuYKy`B6u$l@JP+1a3zU%NNpFig6Fpp#}x_^OB7F{ z9as@v>paw_6+BM9I19M1dr{`*z6TJB%z&Fp`Ome>s5DlWO)OX+;T;@W|D*qG(qiO} zPEi-h0{|Uv;zD~rx|5&I|MO(3Z#GPTBtNE4JX1Bm&dYSS=tF=hnF)3z?(9!?q+)qD zxiJA^N|194Lw-=wu;iFJSDeH!PF-SdfVW^!6L2Xp&u>lvDf&@ z+2>!X@vciAb=l-Ufv63BzpqS?uVSZ;Q^LR$-)QOfk$E0oLSq*|YIitGh|$R{zagJf zA0n(6ymt^sLqJqK>HC$9hkaKkHehmjceb=ok+o!o@p3-Lbqf)dK8WuGO=#C>DBUFWV zPJ=;b;er)8@?b>PgS;iW=#=pCA=?b8{`g~80lP>gu?AeD$P*J$hNs=OQxWi6B{CyC zAUP@g$RxP0mLwV-Z?~Y&(JG3}ND5yVEtPS<9hs6W&40WnC>+jrQCQkse(XuvNZy&* z%nK)Y{IvRR+>mF?IvFZ#0KO%6xB zHYgqCi+jrF4SsAv5j}Yfg+UY;xPk0qcbCFnE$rkH&@AD>5xRG7D^5>wa}4RoTP!jj#Ld2m)H zA$E&T%Mb5am6`M0T~k_xd}YKoJFd3HLSAOX9{tleRWNnf?Xf=*m0V)fHY@zT(x%PC zBV@z12wk~&mh8(Ro6>R}d$%p@f!>P;Mpwk{CMy~^$c_^VNcRpbs80w%o9X`}55vAA0K_rSC!!6v9s9X^{_D4Yi0P8Fd35HX( zwnhr-2+h%rrSh-G*eNqTjqheOkh0^1=KPIas#Suc?4lki@E^}gAW=tTco=yi!)rxdX38&n(-m&GgA@ zjXT)}uJ1|YJhd*_lx}}Hv(E+^zZ@$%T3~ji(85uz;1!U=s!6es)q`|8e!rc+seVDN zQ?V4sON{y8&hP55gZs|-v1E?$d0zstQ)Xy4FxIb4OI-%gMIl#Rp3?*lh`+ojFJk0w%sUS>9*ZYc-_37f$GWot2 z=*r`b0(0I(R29}@cbC>s3pdF5Qt?%xs6mBOHA*hzF43VHg^8I>O!6dbznksQcBWew zOii$&@L@<}R;u!>|4a%t62Zh(eBK9&P(Us+r1n*x+88KI);yG zbG#+X=dz=w%)_m@5jRU@)U#8P-p zrq!-@`if)mBKOL}RO-5Rr|LYP+ZNkY?X(hSenj@rQx;u diff --git a/src/Features/CloudShadows.h b/src/Features/CloudShadows.h index 08c48cebcd..b97a726bb0 100644 --- a/src/Features/CloudShadows.h +++ b/src/Features/CloudShadows.h @@ -17,7 +17,7 @@ struct CloudShadows : Feature virtual inline std::string GetName() override { return "Cloud Shadows"; } virtual inline std::string GetShortName() override { return "CloudShadows"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } - virtual std::string_view GetCategory() const override { return "Sky"; } + virtual std::string_view GetCategory() const override { return "Sky & Weather"; } virtual inline std::string_view GetShaderDefineName() override { return "CLOUD_SHADOWS"; } virtual std::pair> GetFeatureSummary() override { diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index 90546865d6..f4b99e0313 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -9,7 +9,7 @@ struct SkySync : Feature public: virtual inline std::string GetName() override { return "Sky Sync"; } virtual inline std::string GetShortName() override { return "SkySync"; } - virtual std::string_view GetCategory() const override { return "Sky"; } + virtual std::string_view GetCategory() const override { return "Sky & Weather"; } virtual std::pair> GetFeatureSummary() override { diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp index c96cea5095..a64f91f182 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/WeatherEditor.cpp @@ -45,21 +45,8 @@ void LerpDirectional(RE::BGSDirectionalAmbientLightingColors::Directional& oldCo void WeatherEditor::DrawSettings() { - ImGui::TextWrapped("Weather Editor provides controls for saving settings and opening the weather editor window."); - ImGui::Spacing(); - - if (ImGui::BeginTable("##WeatherEditorButtons", 2, ImGuiTableFlags_SizingStretchSame)) { - ImGui::TableNextColumn(); - if (ImGui::Button("Open Editor", { -1, 0 })) { - EditorWindow::GetSingleton()->open = true; - } - - ImGui::TableNextColumn(); - if (ImGui::Button("Save Settings", { -1, 0 })) { - State::GetSingleton()->Save(); - } - - ImGui::EndTable(); + if (ImGui::Button("Open Editor", { -1, 0 })) { + EditorWindow::GetSingleton()->open = true; } ImGui::Spacing(); @@ -67,12 +54,6 @@ void WeatherEditor::DrawSettings() ImGui::Spacing(); DrawWeatherStatusPanel(); - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - DrawQuickWeatherSpawner(); } void WeatherEditor::Bind() @@ -255,90 +236,38 @@ void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newW void WeatherEditor::DrawWeatherStatusPanel() { - if (ImGui::CollapsingHeader("Current Weather Status", ImGuiTreeNodeFlags_DefaultOpen)) { - auto weatherManager = WeatherManager::GetSingleton(); - auto currentWeathers = weatherManager->GetCurrentWeathers(); - - if (currentWeathers.currentWeather) { - ImGui::Text("Current Weather: %s", - currentWeathers.currentWeather->GetFormEditorID() ? - currentWeathers.currentWeather->GetFormEditorID() : - std::format("{:08X}", currentWeathers.currentWeather->GetFormID()).c_str()); - - if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { - ImGui::Text("Transitioning From: %s", - currentWeathers.lastWeather->GetFormEditorID() ? - currentWeathers.lastWeather->GetFormEditorID() : - std::format("{:08X}", currentWeathers.lastWeather->GetFormID()).c_str()); - - ImGui::ProgressBar(currentWeathers.lerpFactor, ImVec2(-1, 0), - std::format("Transition: {:.1f}%%", currentWeathers.lerpFactor * 100.0f).c_str()); - } else { - ImGui::Text("Transition: Complete (100%%)"); - } - - // Show if weather has custom settings - if (weatherManager->HasWeatherSettings(currentWeathers.currentWeather)) { - ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has Custom Settings"); - } else { - ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using Default Settings"); - } - } else { - ImGui::TextColored({ 1.0f, 0.5f, 0.0f, 1.0f }, "No Active Weather"); - } - } -} + ImGui::Text("Current Weather Status"); + ImGui::Separator(); + ImGui::Spacing(); -void WeatherEditor::DrawQuickWeatherSpawner() -{ - if (ImGui::CollapsingHeader("Quick Weather Spawner")) { - ImGui::TextWrapped("Quickly test different weather conditions. Note: Weather changes may take time to transition."); - ImGui::Spacing(); - - static char weatherFilter[128] = ""; - ImGui::InputTextWithHint("##WeatherFilter", "Search weathers...", weatherFilter, sizeof(weatherFilter)); - - if (ImGui::BeginChild("WeatherList", ImVec2(0, 200), true)) { - auto dataHandler = RE::TESDataHandler::GetSingleton(); - if (dataHandler) { - auto& weatherArray = dataHandler->GetFormArray(); - - for (auto* weather : weatherArray) { - if (!weather) - continue; - - const char* editorID = weather->GetFormEditorID(); - std::string displayName = editorID ? editorID : std::format("{:08X}", weather->GetFormID()); - - // Filter - if (weatherFilter[0] != '\0' && - displayName.find(weatherFilter) == std::string::npos) { - continue; - } - - if (ImGui::Selectable(displayName.c_str())) { - // Force weather change - auto sky = RE::Sky::GetSingleton(); - if (sky) { - sky->ForceWeather(weather, false); - } - } - - // Show if this weather has custom settings - if (WeatherManager::GetSingleton()->HasWeatherSettings(weather)) { - ImGui::SameLine(); - ImGui::TextColored({ 0.0f, 0.8f, 0.0f, 1.0f }, "[Modified]"); - } - } - } + auto weatherManager = WeatherManager::GetSingleton(); + auto currentWeathers = weatherManager->GetCurrentWeathers(); + + if (currentWeathers.currentWeather) { + ImGui::Text("Current Weather: %s", + currentWeathers.currentWeather->GetFormEditorID() ? + currentWeathers.currentWeather->GetFormEditorID() : + std::format("{:08X}", currentWeathers.currentWeather->GetFormID()).c_str()); + + if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { + ImGui::Text("Transitioning From: %s", + currentWeathers.lastWeather->GetFormEditorID() ? + currentWeathers.lastWeather->GetFormEditorID() : + std::format("{:08X}", currentWeathers.lastWeather->GetFormID()).c_str()); + + ImGui::ProgressBar(currentWeathers.lerpFactor, ImVec2(-1, 0), + std::format("Transition: {:.1f}%%", currentWeathers.lerpFactor * 100.0f).c_str()); + } else { + ImGui::Text("Transition: Complete (100%%)"); } - ImGui::EndChild(); - if (ImGui::Button("Reset to Natural Weather", ImVec2(-1, 0))) { - auto sky = RE::Sky::GetSingleton(); - if (sky) { - sky->ReleaseWeatherOverride(); - } + // Show if weather has custom settings + if (weatherManager->HasWeatherSettings(currentWeathers.currentWeather)) { + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has Custom Settings"); + } else { + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "Using Default Settings"); } + } else { + ImGui::TextColored({ 1.0f, 0.5f, 0.0f, 1.0f }, "No Active Weather"); } } diff --git a/src/Features/WeatherEditor.h b/src/Features/WeatherEditor.h index 5b7704ec33..2f471a3b70 100644 --- a/src/Features/WeatherEditor.h +++ b/src/Features/WeatherEditor.h @@ -16,15 +16,14 @@ struct WeatherEditor : Feature virtual inline std::string GetName() override { return "Weather Editor"; } virtual inline std::string GetShortName() override { return "WeatherEditor"; } virtual inline std::string_view GetShaderDefineName() override { return "WEATHER"; } - virtual inline std::string_view GetCategory() const override { return "Debug"; } + virtual inline std::string_view GetCategory() const override { return "Sky & Weather"; } virtual inline std::pair> GetFeatureSummary() override { return { - "Development tool for editing weather settings and testing IBL.", + "Development tool for editing weather, testing weather transitions, and managing weather-related feature settings.", { "Provides weather editing functionality", - "Includes save/load settings controls", - "Real-time weather transition monitoring", - "Debug feature for development" } + "Includes dynamic saving and loading of imagespace, weather and lighting template settings.", + "Real-time editing and previewing of effects" } }; } @@ -43,5 +42,4 @@ struct WeatherEditor : Feature private: void DrawWeatherStatusPanel(); - void DrawQuickWeatherSpawner(); }; diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 063ed03d5b..4ea63685eb 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -507,6 +507,19 @@ void WeatherPicker::RenderWeatherControls(RE::Sky* sky) ImGui::EndCombo(); } + // Instantly Change Weather button + if (ImGui::Button("Instantly Change Weather", ImVec2(-1, 0))) { + if (s_selectedWeatherIdx >= 0 && s_selectedWeatherIdx < s_filteredWeathers.size()) { + auto selectedWeather = s_filteredWeathers[s_selectedWeatherIdx]; + sky->ForceWeather(selectedWeather, false); + logger::info("[WeatherPicker] Instantly forced weather to: {}", Util::FormatWeather(selectedWeather)); + } + } + if (auto _tt = Util::HoverTooltipWrapper()) { + Util::DrawMultiLineTooltip({ "Immediately forces the selected weather without transition.", + "This bypasses the normal transition system for instant testing." }); + } + ImGui::Spacing(); } diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h index defef2586a..7a7ca03624 100644 --- a/src/Features/WeatherPicker.h +++ b/src/Features/WeatherPicker.h @@ -11,7 +11,7 @@ struct WeatherPicker : OverlayFeature virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } - virtual std::string_view GetCategory() const override { return "Debug"; } + virtual std::string_view GetCategory() const override { return "Sky & Weather"; } virtual bool IsInMenu() const override { return true; } // Show in main menu to provide weather debugging UI virtual std::pair> GetFeatureSummary() override; diff --git a/src/Menu.cpp b/src/Menu.cpp index f7862d0b9c..bcea61d38d 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -179,6 +179,7 @@ Menu::~Menu() uiIcons.clearCache.Release(); uiIcons.logo.Release(); uiIcons.featureSettingRevert.Release(); + uiIcons.applyToGame.Release(); uiIcons.discord.Release(); uiIcons.characters.Release(); uiIcons.display.Release(); @@ -730,7 +731,7 @@ void Menu::ProcessInputEventQueue() } } } - if (key == VK_ESCAPE && IsEnabled) { + if (key == VK_ESCAPE && IsEnabled && !EditorWindow::GetSingleton()->open) { IsEnabled = false; } } diff --git a/src/Menu.h b/src/Menu.h index d9470072bb..45aacf01c2 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -186,10 +186,12 @@ class Menu { UIIcon saveSettings; UIIcon loadSettings; + UIIcon deleteSettings; UIIcon clearCache; UIIcon logo; // New logo icon UIIcon search; // Search icon for search bars UIIcon featureSettingRevert; // Feature revert settings icon + UIIcon applyToGame; // Apply changes to game icon (weather editor) // Social media/external link icons UIIcon discord; diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index b3a5800db7..6ee0d85aa7 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -118,7 +118,7 @@ std::vector FeatureListRenderer::BuildMenuLis } // Define category order - std::vector categoryOrder = { "Display", "Debug", "Characters", "Grass", "Lighting", "Materials", "Post-Processing", "Sky", "Landscape & Textures", "Water", "Other" }; + std::vector categoryOrder = { "Display", "Debug", "Characters", "Grass", "Lighting", "Materials", "Post-Processing", "Sky & Weather", "Landscape & Textures", "Water", "Other" }; // Add categorized features to menu with collapsible headers for (const std::string& category : categoryOrder) { if (categorizedFeatures.find(category) != categorizedFeatures.end() && !categorizedFeatures[category].empty()) { diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 5ddb981d30..3a8b6c8cce 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -177,12 +177,14 @@ namespace Util logger::info("InitializeMenuIcons: Loading icons from base path: {}", basePath); // Initialize all texture pointers to nullptr for safe cleanup - std::array texturePointers = { + std::array texturePointers = { &menu->uiIcons.saveSettings.texture, &menu->uiIcons.loadSettings.texture, + &menu->uiIcons.deleteSettings.texture, &menu->uiIcons.clearCache.texture, &menu->uiIcons.logo.texture, &menu->uiIcons.featureSettingRevert.texture, + &menu->uiIcons.applyToGame.texture, &menu->uiIcons.discord.texture, &menu->uiIcons.characters.texture, &menu->uiIcons.display.texture, @@ -228,9 +230,11 @@ namespace Util // Load action icons loadIconWithLogging(basePath + "Action Icons\\save-settings.png", &menu->uiIcons.saveSettings.texture, menu->uiIcons.saveSettings.size, "save-settings"); loadIconWithLogging(basePath + "Action Icons\\load-settings.png", &menu->uiIcons.loadSettings.texture, menu->uiIcons.loadSettings.size, "load-settings"); + loadIconWithLogging(basePath + "Action Icons\\delete.png", &menu->uiIcons.deleteSettings.texture, menu->uiIcons.deleteSettings.size, "delete"); loadIconWithLogging(basePath + "Action Icons\\clear-cache.png", &menu->uiIcons.clearCache.texture, menu->uiIcons.clearCache.size, "clear-cache"); loadIconWithLogging(basePath + "Community Shaders Logo\\cs-logo.png", &menu->uiIcons.logo.texture, menu->uiIcons.logo.size, "logo"); loadIconWithLogging(basePath + "Action Icons\\restore-settings.png", &menu->uiIcons.featureSettingRevert.texture, menu->uiIcons.featureSettingRevert.size, "restore-settings"); + loadIconWithLogging(basePath + "Action Icons\\apply-to-game.png", &menu->uiIcons.applyToGame.texture, menu->uiIcons.applyToGame.size, "apply-to-game"); loadIconWithLogging(basePath + "Action Icons\\discord.png", &menu->uiIcons.discord.texture, menu->uiIcons.discord.size, "discord"); // Load category icons in a more compact way @@ -397,7 +401,7 @@ namespace Util categoryIcon = menu.grass.texture; } else if (strcmp(categoryName, "Lighting") == 0) { categoryIcon = menu.lighting.texture; - } else if (strcmp(categoryName, "Sky") == 0) { + } else if (strcmp(categoryName, "Sky & Weather") == 0) { categoryIcon = menu.sky.texture; } else if (strcmp(categoryName, "Landscape & Textures") == 0) { categoryIcon = menu.landscape.texture; diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 276088cce2..292ae4e255 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -2,6 +2,7 @@ #include "State.h" #include "Features/WeatherEditor.h" +#include "Weather/LightingTemplateWidget.h" #include "imgui_internal.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges) @@ -63,25 +64,16 @@ void EditorWindow::ShowObjectsWindow() // Left column: Categories ImGui::TableSetColumnIndex(0); - // Begin a table for the categories list - if (ImGui::BeginTable("CategoriesTable", 1, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Categories"); // Label for the table + ImGui::Text("Categories"); + ImGui::Spacing(); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - - // List of categories - const char* categories[] = { "Lighting Template", "Weather", "WorldSpace" }; - for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { - // Highlight the selected category - if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { - selectedCategory = categories[i]; // Update selected category - } + // List of categories + const char* categories[] = { "ImageSpace", "Lighting Template", "Weather", "WorldSpace" }; + for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { + // Highlight the selected category + if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { + selectedCategory = categories[i]; // Update selected category } - - ImGui::EndTable(); } // Right column: Objects @@ -92,61 +84,71 @@ void EditorWindow::ShowObjectsWindow() ImGui::SameLine(); HelpMarker("Type a part of an object name to filter the list."); - // Create a table for the right column with "Name" and "ID" headers - if (ImGui::BeginTable("DetailsTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { - ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch); // Added File column - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch); + // Create a table for the right column with "Name" and "ID" headers. Different weights to prevent truncation. + if (ImGui::BeginTable("DetailsTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f); // Largest - weather/template names + ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 80.0f); // Fixed - 8 hex chars + ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Medium - plugin names + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f); // Smaller - status text ImGui::TableHeadersRow(); - // Display objects based on the selected category - auto& widgets = selectedCategory == "Weather" ? weatherWidgets : - selectedCategory == "WorldSpace" ? worldSpaceWidgets : - lightingTemplateWidgets; + // Display objects based on the selected category + auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + selectedCategory == "ImageSpace" ? imageSpaceWidgets : + lightingTemplateWidgets; + + // Get current cell's lighting template for prioritization + RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; + if (selectedCategory == "Lighting Template") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto& cellData = player->parentCell->GetRuntimeData(); + currentCellLightingTemplate = cellData.lightingTemplate; + } + } - // Filtered display of widgets + // Filtered display of widgets - show current cell's lighting template first + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { for (int i = 0; i < widgets.size(); ++i) { + auto* ltWidget = dynamic_cast(widgets[i]); + if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) + continue; + if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) - continue; // Skip widgets that don't match the filter + continue; - auto editorLabel = widgets[i]->GetEditorID(); - auto markedRecord = settings.markedRecords.find(editorLabel); + auto editorLabel = std::format("[CURRENT] {}", widgets[i]->GetEditorID()); + auto markedRecord = settings.markedRecords.find(widgets[i]->GetEditorID()); ImGui::TableNextRow(); - // Set background colour - if (markedRecord != settings.markedRecords.end()) { - auto& color = settings.recordMarkers[markedRecord->second]; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); - } else { - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGuiCol_TableRowBg); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGuiCol_TableRowBgAlt); - } + // Highlight current cell's lighting template + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); ImGui::TableSetColumnIndex(0); - // Editor ID column + // Editor ID column with [CURRENT] prefix if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { widgets[i]->SetOpen(true); } } - // Opens a context menu on right click to mark records by color + // Context menu if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; for (auto& recordMarker : settings.recordMarkers) { if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[editorLabel] = recordMarker.first; + settings.markedRecords[widgets[i]->GetEditorID()] = recordMarker.first; Save(); } } if (ImGui::MenuItem("Remove")) { - markedRecords.erase(editorLabel); + markedRecords.erase(widgets[i]->GetEditorID()); Save(); } @@ -163,15 +165,82 @@ void EditorWindow::ShowObjectsWindow() // Status column ImGui::TableNextColumn(); - - // Re-check if the record exists after potential removal - markedRecord = settings.markedRecords.find(editorLabel); if (markedRecord != settings.markedRecords.end()) { ImGui::Text("%s", markedRecord->second.c_str()); } } + } + + // Filtered display of widgets - regular list + for (int i = 0; i < widgets.size(); ++i) { + // Skip current cell's lighting template if already shown + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + auto* ltWidget = dynamic_cast(widgets[i]); + if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) + continue; + } + + if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) + continue; // Skip widgets that don't match the filter + + auto editorLabel = widgets[i]->GetEditorID(); + auto markedRecord = settings.markedRecords.find(editorLabel); + ImGui::TableNextRow(); + + // Set background colour + if (markedRecord != settings.markedRecords.end()) { + auto& color = settings.recordMarkers[markedRecord->second]; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); + } + + ImGui::TableSetColumnIndex(0); + + ImGui::TableSetColumnIndex(0); + + // Editor ID column + if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + widgets[i]->SetOpen(true); + } + } + + // Opens a context menu on right click to mark records by color + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; + + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[editorLabel] = recordMarker.first; + Save(); + } + } + + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(editorLabel); + Save(); + } + + ImGui::EndPopup(); + } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text(widgets[i]->GetFormID().c_str()); - ImGui::EndTable(); + // File column + ImGui::TableNextColumn(); + ImGui::Text(widgets[i]->GetFilename().c_str()); + + // Status column + ImGui::TableNextColumn(); + + // Re-check if the record exists after potential removal + markedRecord = settings.markedRecords.find(editorLabel); + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text("%s", markedRecord->second.c_str()); + } + } ImGui::EndTable(); } ImGui::EndTable(); @@ -232,6 +301,12 @@ void EditorWindow::ShowWidgetWindow() if (widget->IsOpen()) widget->DrawWidget(); } + + for (int i = 0; i < (int)imageSpaceWidgets.size(); i++) { + auto widget = imageSpaceWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } } void EditorWindow::RenderUI() @@ -245,8 +320,8 @@ void EditorWindow::RenderUI() // Increase background opacity for all editor windows ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); - // Check for Escape key to close editor - if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + // Check for Escape key to close editor (but not if a popup is open) + if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) && !ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) { open = false; } @@ -255,6 +330,58 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Save All Open Widgets", "Ctrl+S")) { SaveAll(); } + + // Save individual widgets submenu + if (ImGui::BeginMenu("Save")) { + bool hasOpenWidgets = false; + + // Weather widgets + for (auto* widget : weatherWidgets) { + if (widget->IsOpen()) { + hasOpenWidgets = true; + if (ImGui::MenuItem(std::format("Save Weather_{}", widget->GetEditorID()).c_str())) { + widget->Save(); + } + } + } + + // WorldSpace widgets + for (auto* widget : worldSpaceWidgets) { + if (widget->IsOpen()) { + hasOpenWidgets = true; + if (ImGui::MenuItem(std::format("Save WorldSpace_{}", widget->GetEditorID()).c_str())) { + widget->Save(); + } + } + } + + // Lighting Template widgets + for (auto* widget : lightingTemplateWidgets) { + if (widget->IsOpen()) { + hasOpenWidgets = true; + if (ImGui::MenuItem(std::format("Save Lighting_{}", widget->GetEditorID()).c_str())) { + widget->Save(); + } + } + } + + // ImageSpace widgets + for (auto* widget : imageSpaceWidgets) { + if (widget->IsOpen()) { + hasOpenWidgets = true; + if (ImGui::MenuItem(std::format("Save ImageSpace_{}", widget->GetEditorID()).c_str())) { + widget->Save(); + } + } + } + + if (!hasOpenWidgets) { + ImGui::TextDisabled("No open widgets"); + } + + ImGui::EndMenu(); + } + ImGui::Separator(); if (ImGui::MenuItem("Close All Weather Widgets")) { for (auto* widget : weatherWidgets) widget->SetOpen(false); @@ -265,10 +392,13 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Close All Lighting Widgets")) { for (auto* widget : lightingTemplateWidgets) widget->SetOpen(false); } + if (ImGui::MenuItem("Close All ImageSpace Widgets")) { + for (auto* widget : imageSpaceWidgets) widget->SetOpen(false); + } ImGui::EndMenu(); } if (ImGui::BeginMenu("Settings")) { - if (ImGui::MenuItem("Editor Preferences")) { + if (ImGui::MenuItem("Editor Flags")) { showSettingsWindow = !showSettingsWindow; } ImGui::Separator(); @@ -309,6 +439,14 @@ void EditorWindow::RenderUI() } } } + for (auto* widget : imageSpaceWidgets) { + if (widget->IsOpen()) { + openCount++; + if (ImGui::MenuItem(std::format("ImageSpace: {}", widget->GetEditorID()).c_str())) { + // Focus window + } + } + } if (openCount == 0) { ImGui::TextDisabled("No widgets open"); @@ -329,6 +467,7 @@ void EditorWindow::RenderUI() ImGui::BulletText("Weathers: %d", (int)weatherWidgets.size()); ImGui::BulletText("WorldSpaces: %d", (int)worldSpaceWidgets.size()); ImGui::BulletText("Lighting: %d", (int)lightingTemplateWidgets.size()); + ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); ImGui::EndMenu(); } @@ -386,6 +525,9 @@ void EditorWindow::RenderUI() ShowWidgetWindow(); + // Render notifications on top of everything + RenderNotifications(); + // Pop the alpha style var ImGui::PopStyleVar(); } @@ -418,6 +560,14 @@ void EditorWindow::SetupResources() widget->Load(); lightingTemplateWidgets.push_back(widget); } + + auto& imageSpaceArray = dataHandler->GetFormArray(); + + for (auto imageSpace : imageSpaceArray) { + auto widget = new ImageSpaceWidget(imageSpace); + widget->Load(); + imageSpaceWidgets.push_back(widget); + } } void EditorWindow::Draw() @@ -484,6 +634,11 @@ void EditorWindow::SaveAll() lightingTemplate->Save(); } + for (auto imageSpace : imageSpaceWidgets) { + if (imageSpace->IsOpen()) + imageSpace->Save(); + } + Save(); } @@ -503,7 +658,7 @@ void EditorWindow::ShowSettingsWindow() ImGui::Begin("Settings", &showSettingsWindow); // Static variable to track the selected category - static std::string selectedOption = "Preferences"; + static std::string selectedOption = "Flags"; // Create a table with two columns if (ImGui::BeginTable("SettingsTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner | ImGuiTableFlags_NoHostExtendX)) { @@ -516,7 +671,7 @@ void EditorWindow::ShowSettingsWindow() // Left column: Options ImGui::TableSetColumnIndex(0); // List of options - const char* options[] = { "Preferences" }; + const char* options[] = { "Flags" }; for (int i = 0; i < IM_ARRAYSIZE(options); ++i) { if (ImGui::Selectable(options[i], selectedOption == options[i])) { selectedOption = options[i]; // Update selected option @@ -527,29 +682,92 @@ void EditorWindow::ShowSettingsWindow() ImGui::TableSetColumnIndex(1); // Create a table for the right column. - if (selectedOption == "Preferences") { - if (ImGui::BeginTable("PreferencesTable", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { + if (selectedOption == "Flags") { + if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Colour", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 60.0f); auto& recordMarkers = settings.recordMarkers; + + // Store markers to delete (can't delete while iterating) + static std::string markerToDelete; + markerToDelete.clear(); + + // Store rename info (old name -> new name) + static std::pair renameInfo; + static bool needsRename = false; for (auto& recordMarker : recordMarkers) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - ImGui::Text(recordMarker.first.c_str()); + + // Editable label + static char labelBuffer[256]; + strncpy_s(labelBuffer, recordMarker.first.c_str(), sizeof(labelBuffer) - 1); + labelBuffer[sizeof(labelBuffer) - 1] = '\0'; + + ImGui::SetNextItemWidth(-1); + if (ImGui::InputText(std::format("##Label{}", recordMarker.first).c_str(), labelBuffer, sizeof(labelBuffer))) { + // Mark for rename + renameInfo = { recordMarker.first, std::string(labelBuffer) }; + needsRename = true; + } ImGui::TableSetColumnIndex(1); if (ImGui::ColorEdit4(std::format("Color##{}", recordMarker.first).c_str(), (float*)&recordMarker.second)) { Save(); - }; + } + + ImGui::TableSetColumnIndex(2); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { + markerToDelete = recordMarker.first; + } + ImGui::PopStyleColor(); + } + + // Process rename + if (needsRename && renameInfo.first != renameInfo.second && !renameInfo.second.empty()) { + // Check if new name doesn't already exist + if (recordMarkers.find(renameInfo.second) == recordMarkers.end()) { + auto color = recordMarkers[renameInfo.first]; + recordMarkers.erase(renameInfo.first); + recordMarkers[renameInfo.second] = color; + + // Update any records that were using the old marker name + for (auto& [recordId, markerName] : settings.markedRecords) { + if (markerName == renameInfo.first) { + markerName = renameInfo.second; + } + } + + Save(); + } + needsRename = false; + } + + // Process deletion + if (!markerToDelete.empty()) { + recordMarkers.erase(markerToDelete); + + // Remove any records that were using this marker + for (auto it = settings.markedRecords.begin(); it != settings.markedRecords.end();) { + if (it->second == markerToDelete) { + it = settings.markedRecords.erase(it); + } else { + ++it; + } + } + + Save(); } ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); if (recordMarkers.size() < maxRecordMarkers && ImGui::Selectable("Add new marker")) { - recordMarkers.insert({ std::string("New marker##new{}", recordMarkers.size()), { 0, 0, 0, 255 } }); + recordMarkers.insert({ std::format("New marker {}", recordMarkers.size()), { 0.5f, 0.5f, 0.5f, 1.0f } }); Save(); } @@ -700,3 +918,60 @@ void EditorWindow::RestoreVanityCamera() logger::info("Vanity camera restored (delay: {})", savedVanityCameraDelay); } } + +void EditorWindow::ShowNotification(const std::string& message, const ImVec4& color, float duration) +{ + Notification notif; + notif.message = message; + notif.color = color; + notif.startTime = static_cast(ImGui::GetTime()); + notif.duration = duration; + notifications.push_back(notif); +} + +void EditorWindow::RenderNotifications() +{ + float currentTime = static_cast(ImGui::GetTime()); + float yOffset = 10.0f; + + // Remove expired notifications + notifications.erase( + std::remove_if(notifications.begin(), notifications.end(), + [currentTime](const Notification& n) { return currentTime - n.startTime > n.duration; }), + notifications.end()); + + // Render active notifications + for (auto& notif : notifications) { + float elapsed = currentTime - notif.startTime; + float fadeStart = notif.duration - 0.5f; // Start fading 0.5s before end + float alpha = 1.0f; + + // Fade out in the last 0.5 seconds + if (elapsed > fadeStart) { + alpha = 1.0f - ((elapsed - fadeStart) / 0.5f); + } + + // Position in top-left corner + ImGui::SetNextWindowPos(ImVec2(10.0f, yOffset), ImGuiCond_Always); + ImGui::SetNextWindowBgAlpha(0.8f * alpha); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(15.0f, 10.0f)); + + if (ImGui::Begin(std::format("##Notification{}", (void*)¬if).c_str(), + nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { + + ImVec4 colorWithAlpha = notif.color; + colorWithAlpha.w *= alpha; + ImGui::PushStyleColor(ImGuiCol_Text, colorWithAlpha); + ImGui::TextUnformatted(notif.message.c_str()); + ImGui::PopStyleColor(); + + yOffset += ImGui::GetWindowSize().y + 5.0f; + } + ImGui::End(); + + ImGui::PopStyleVar(2); + } +} diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index 2c17f5fc73..97545ee68c 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -2,6 +2,7 @@ #include "Buffer.h" +#include "Weather/ImageSpaceWidget.h" #include "Weather/LightingTemplateWidget.h" #include "Weather/WeatherWidget.h" #include "Weather/WorldSpaceWidget.h" @@ -25,6 +26,7 @@ class EditorWindow std::vector weatherWidgets; std::vector worldSpaceWidgets; std::vector lightingTemplateWidgets; + std::vector imageSpaceWidgets; // Weather locking for editing RE::TESWeather* lockedWeather = nullptr; @@ -62,25 +64,40 @@ class EditorWindow void DisableVanityCamera(); void RestoreVanityCamera(); + // Notification system + struct Notification + { + std::string message; + ImVec4 color; + float startTime; + float duration; + }; + std::vector notifications; + + void ShowNotification(const std::string& message, const ImVec4& color = ImVec4(1.0f, 0.0f, 0.0f, 1.0f), float duration = 3.0f); + void RenderNotifications(); + struct Settings { std::map recordMarkers = { - { "TO-DO", { 0.9f, 0.15, 0.15, 1 } }, - { "In-progress", { 0.5f, 0.8f, 0.0f, 1 } }, - { "Done", { 0.05f, 0.85f, 0.3f, 1 } } + { "To Do", { 1.0f, 0.0f, 0.0f, 1.0f } }, + { "In Progress", { 190.0f / 255.0f, 155.0f / 255.0f, 0.0f, 1.0f } }, + { "Complete", { 0.0f, 130.0f / 255.0f, 0.0f, 1.0f } } }; std::map markedRecords; bool autoApplyChanges = true; + bool suppressDeleteWarning = false; }; Settings settings; + void Save(); + private: void SaveAll(); void SaveSettings(); void LoadSettings(); void ShowSettingsWindow(); - void Save(); void Load(); json j; std::string settingsFilename = "EditorSettings"; diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp new file mode 100644 index 0000000000..abded0654b --- /dev/null +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -0,0 +1,310 @@ +#include "ImageSpaceWidget.h" + +#include "../EditorWindow.h" +#include "Util.h" + +#include + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( + ImageSpaceWidget::Settings, + hdrEyeAdaptSpeed, + hdrBloomBlurRadius, + hdrBloomThreshold, + hdrBloomScale, + hdrSunlightScale, + hdrSkyScale, + cinematicSaturation, + cinematicBrightness, + cinematicContrast, + tintColor, + tintAmount, + dofStrength, + dofDistance, + dofRange) + +ImageSpaceWidget::~ImageSpaceWidget() +{ +} + +void ImageSpaceWidget::DrawWidget() +{ + if (!open) + return; + + auto editorWindow = EditorWindow::GetSingleton(); + + ImGui::SetNextWindowSize(ImVec2(600, 800), ImGuiCond_FirstUseEver); + if (ImGui::Begin(std::format("ImageSpace Editor - {}", GetEditorID()).c_str(), &open)) { + ImGui::Text("Form ID: %s", GetFormID().c_str()); + ImGui::Text("Plugin: %s", GetFilename().c_str()); + ImGui::Separator(); + + // Draw settings sections + DrawHDRSettings(); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + DrawCinematicSettings(); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + DrawTintSettings(); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + DrawDOFSettings(); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + // Action buttons + ImGui::BeginDisabled(!editorWindow->settings.autoApplyChanges); + if (ImGui::Button("Apply", ImVec2(-1, 0))) { + ApplyChanges(); + } + ImGui::EndDisabled(); + + if (ImGui::Button("Revert", ImVec2(-1, 0))) { + RevertChanges(); + } + + // Save/Load buttons (always visible) + if (ImGui::Button("Save to File", ImVec2(-1, 0))) { + Save(); + } + + if (ImGui::Button("Load from File", ImVec2(-1, 0))) { + Load(); + } + + // Delete button with confirmation (only show if file exists) + std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName(), GetEditorID()); + if (std::filesystem::exists(filePath)) { + static bool showDeleteConfirm = false; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::ImageButton("##DeleteImageSpace", globals::menu->uiIcons.deleteSettings.texture, ImVec2(32, 32))) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + showDeleteConfirm = true; + } + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete saved file and revert to defaults"); + } + + if (showDeleteConfirm) { + ImGui::OpenPopup("Delete Confirmation##ImageSpace"); + } + + if (ImGui::BeginPopupModal("Delete Confirmation##ImageSpace", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Are you sure you want to delete the settings file?"); + ImGui::Text("This will reload default values from the game."); + ImGui::Spacing(); + + ImGui::Checkbox("Don't ask again", &editorWindow->settings.suppressDeleteWarning); + + ImGui::Spacing(); + if (ImGui::Button("Delete", ImVec2(120, 0))) { + Delete(); + showDeleteConfirm = false; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + showDeleteConfirm = false; + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + } + ImGui::End(); +} + +void ImageSpaceWidget::DrawHDRSettings() +{ + if (ImGui::CollapsingHeader("HDR Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat("Eye Adapt Speed", &settings.hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f"); + ImGui::SliderFloat("Bloom Blur Radius", &settings.hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f"); + ImGui::SliderFloat("Bloom Threshold", &settings.hdrBloomThreshold, 0.0f, 10.0f, "%.3f"); + ImGui::SliderFloat("Bloom Scale", &settings.hdrBloomScale, 0.0f, 10.0f, "%.3f"); + ImGui::SliderFloat("Sunlight Scale", &settings.hdrSunlightScale, 0.0f, 10.0f, "%.3f"); + ImGui::SliderFloat("Sky Scale", &settings.hdrSkyScale, 0.0f, 10.0f, "%.3f"); + + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } +} + +void ImageSpaceWidget::DrawCinematicSettings() +{ + if (ImGui::CollapsingHeader("Cinematic Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat("Saturation", &settings.cinematicSaturation, 0.0f, 2.0f, "%.3f"); + ImGui::SliderFloat("Brightness", &settings.cinematicBrightness, 0.0f, 2.0f, "%.3f"); + ImGui::SliderFloat("Contrast", &settings.cinematicContrast, 0.0f, 2.0f, "%.3f"); + + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } +} + +void ImageSpaceWidget::DrawTintSettings() +{ + if (ImGui::CollapsingHeader("Tint Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::ColorEdit3("Tint Color", &settings.tintColor.x); + ImGui::SliderFloat("Tint Amount", &settings.tintAmount, 0.0f, 1.0f, "%.3f"); + + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } +} + +void ImageSpaceWidget::DrawDOFSettings() +{ + if (ImGui::CollapsingHeader("Depth of Field", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::SliderFloat("DOF Strength", &settings.dofStrength, 0.0f, 10.0f, "%.3f"); + ImGui::SliderFloat("DOF Distance", &settings.dofDistance, 0.0f, 10000.0f, "%.1f"); + ImGui::SliderFloat("DOF Range", &settings.dofRange, 0.0f, 10000.0f, "%.1f"); + + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } +} + +void ImageSpaceWidget::LoadSettings() +{ + try { + if (!js.empty() && js.contains("Settings")) { + auto settingsJson = js["Settings"]; + + // Validate that we have actual data + bool hasValidData = false; + for (const auto& [key, value] : settingsJson.items()) { + if (value.is_number() && value.get() != 0.0f) { + hasValidData = true; + break; + } + if (value.is_array() && !value.empty()) { + hasValidData = true; + break; + } + } + + if (hasValidData) { + settings = settingsJson; + logger::info("ImageSpace settings loaded successfully for {}", GetEditorID()); + } else { + logger::warn("ImageSpace settings contained only zero values for {}, loading from form", GetEditorID()); + LoadImageSpaceValues(); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Failed to load {} - using default values", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); + } + } else { + LoadImageSpaceValues(); + } + } catch (const std::exception& e) { + logger::error("Failed to load ImageSpace settings for {}: {}", GetEditorID(), e.what()); + LoadImageSpaceValues(); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Error loading {} - using default values", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); + } +} + +void ImageSpaceWidget::SaveSettings() +{ + try { + js["Settings"] = settings; + logger::info("ImageSpace settings saved for {}", GetEditorID()); + } catch (const std::exception& e) { + logger::error("Failed to save ImageSpace settings for {}: {}", GetEditorID(), e.what()); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Failed to save {}", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); + } +} + +void ImageSpaceWidget::SetImageSpaceValues() +{ + if (!imageSpace) + return; + + auto& data = imageSpace->data; + + // HDR + data.hdr.eyeAdaptSpeed = settings.hdrEyeAdaptSpeed; + data.hdr.bloomBlurRadius = settings.hdrBloomBlurRadius; + data.hdr.bloomThreshold = settings.hdrBloomThreshold; + data.hdr.bloomScale = settings.hdrBloomScale; + data.hdr.sunlightScale = settings.hdrSunlightScale; + data.hdr.skyScale = settings.hdrSkyScale; + + // Cinematic + data.cinematic.saturation = settings.cinematicSaturation; + data.cinematic.brightness = settings.cinematicBrightness; + data.cinematic.contrast = settings.cinematicContrast; + + // Tint + data.tint.color.red = settings.tintColor.x; + data.tint.color.green = settings.tintColor.y; + data.tint.color.blue = settings.tintColor.z; + data.tint.amount = settings.tintAmount; + + // Depth of Field + data.depthOfField.strength = settings.dofStrength; + data.depthOfField.distance = settings.dofDistance; + data.depthOfField.range = settings.dofRange; +} + +void ImageSpaceWidget::LoadImageSpaceValues() +{ + if (!imageSpace) + return; + + auto& data = imageSpace->data; + + // HDR + settings.hdrEyeAdaptSpeed = data.hdr.eyeAdaptSpeed; + settings.hdrBloomBlurRadius = data.hdr.bloomBlurRadius; + settings.hdrBloomThreshold = data.hdr.bloomThreshold; + settings.hdrBloomScale = data.hdr.bloomScale; + settings.hdrSunlightScale = data.hdr.sunlightScale; + settings.hdrSkyScale = data.hdr.skyScale; + + // Cinematic + settings.cinematicSaturation = data.cinematic.saturation; + settings.cinematicBrightness = data.cinematic.brightness; + settings.cinematicContrast = data.cinematic.contrast; + + // Tint + settings.tintColor.x = data.tint.color.red; + settings.tintColor.y = data.tint.color.green; + settings.tintColor.z = data.tint.color.blue; + settings.tintAmount = data.tint.amount; + + // Depth of Field + settings.dofStrength = data.depthOfField.strength; + settings.dofDistance = data.depthOfField.distance; + settings.dofRange = data.depthOfField.range; +} + +void ImageSpaceWidget::ApplyChanges() +{ + SetImageSpaceValues(); +} + +void ImageSpaceWidget::RevertChanges() +{ + LoadImageSpaceValues(); +} diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.h b/src/WeatherEditor/Weather/ImageSpaceWidget.h new file mode 100644 index 0000000000..ee506fc57b --- /dev/null +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.h @@ -0,0 +1,60 @@ +#pragma once + +#include "../Widget.h" + +class ImageSpaceWidget : public Widget +{ +public: + RE::TESImageSpace* imageSpace = nullptr; + + ImageSpaceWidget(RE::TESImageSpace* a_imageSpace) + { + form = a_imageSpace; + imageSpace = a_imageSpace; + LoadImageSpaceValues(); + } + + struct Settings + { + // HDR Settings + float hdrEyeAdaptSpeed = 0.0f; + float hdrBloomBlurRadius = 0.0f; + float hdrBloomThreshold = 0.0f; + float hdrBloomScale = 0.0f; + float hdrSunlightScale = 0.0f; + float hdrSkyScale = 0.0f; + + // Cinematic Settings + float cinematicSaturation = 0.0f; + float cinematicBrightness = 0.0f; + float cinematicContrast = 0.0f; + + // Tint Colors + float3 tintColor = { 1.0f, 1.0f, 1.0f }; + float tintAmount = 0.0f; + + // Depth of Field + float dofStrength = 0.0f; + float dofDistance = 0.0f; + float dofRange = 0.0f; + }; + + Settings settings; + + ~ImageSpaceWidget(); + + virtual void DrawWidget() override; + virtual void LoadSettings() override; + virtual void SaveSettings() override; + + void SetImageSpaceValues(); + void LoadImageSpaceValues(); + void ApplyChanges(); + void RevertChanges(); + +private: + void DrawHDRSettings(); + void DrawCinematicSettings(); + void DrawTintSettings(); + void DrawDOFSettings(); +}; diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index e8dd2afe13..13fa16e931 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -1,26 +1,294 @@ #include "LightingTemplateWidget.h" +#include "../EditorWindow.h" +#include "../WeatherUtils.h" + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LightingTemplateWidget::DirectionalColor, max, min) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LightingTemplateWidget::DALC, specular, fresnelPower, directional) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(LightingTemplateWidget::Settings, + ambient, + directional, + fogColorNear, + fogColorFar, + fogNear, + fogFar, + directionalXY, + directionalZ, + directionalFade, + clipDist, + fogPower, + fogClamp, + lightFadeStart, + lightFadeEnd, + dalc) + LightingTemplateWidget::~LightingTemplateWidget() { } void LightingTemplateWidget::DrawWidget() { + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) { - if (ImGui::BeginMenuBar()) { - if (ImGui::BeginMenu("Menu")) { - ImGui::EndMenu(); + DrawMenu(); + + auto editorWindow = EditorWindow::GetSingleton(); + + if (!editorWindow->settings.autoApplyChanges) { + auto menu = globals::menu; + bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && + menu->uiIcons.saveSettings.texture && + menu->uiIcons.featureSettingRevert.texture; + + if (useIcons) { + const float iconSize = ImGui::GetFrameHeight(); + const ImVec2 buttonSize(iconSize, iconSize); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + + if (ImGui::ImageButton("##ApplyLightingTemplate", menu->uiIcons.saveSettings.texture, buttonSize)) { + ApplyChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + + ImGui::SameLine(); + + if (ImGui::ImageButton("##RevertLightingTemplate", menu->uiIcons.featureSettingRevert.texture, buttonSize)) { + RevertChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } + + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); + if (ImGui::Button("Apply", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { + ApplyChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + if (ImGui::Button("Revert", ImVec2(-1, 0))) { + RevertChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } } - ImGui::EndMenuBar(); + } + ImGui::Separator(); + + if (ImGui::BeginTabBar("LightingTemplateSettingsTabs", ImGuiTabBarFlags_None)) { + if (ImGui::BeginTabItem("Basic")) { + DrawBasicSettings(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Fog")) { + DrawFogSettings(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("DALC")) { + DrawDALCSettings(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); } } ImGui::End(); } +void LightingTemplateWidget::DrawBasicSettings() +{ + bool changed = false; + + if (ImGui::CollapsingHeader("Ambient & Directional", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Spacing(); + if (DrawColorEdit("Ambient Color", settings.ambient)) changed = true; + ImGui::Spacing(); + if (DrawColorEdit("Directional Color", settings.directional)) changed = true; + ImGui::Spacing(); + } + + if (ImGui::CollapsingHeader("Directional Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Spacing(); + if (DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; + ImGui::Spacing(); + } + + if (ImGui::CollapsingHeader("Light Fade", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Spacing(); + if (DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; + ImGui::Spacing(); + } + + if (ImGui::CollapsingHeader("Other", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Spacing(); + if (DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; + ImGui::Spacing(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } +} + +void LightingTemplateWidget::DrawFogSettings() +{ + bool changed = false; + + ImGui::Spacing(); + if (DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; + ImGui::Spacing(); + if (DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; + ImGui::Spacing(); + + ImGui::Spacing(); + if (DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; + ImGui::Spacing(); + + ImGui::Spacing(); + if (DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; + ImGui::Spacing(); + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } +} + +void LightingTemplateWidget::DrawDALCSettings() +{ + bool changed = false; + + ImGui::Spacing(); + if (DrawColorEdit("Specular", settings.dalc.specular)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; + ImGui::Spacing(); + + for (int j = 0; j < 3; j++) { + const char* labels[] = { "X", "Y", "Z" }; + ImGui::Separator(); + ImGui::Text("Directional %s", labels[j]); + if (DrawColorEdit(std::format("Max##{}", labels[j]), settings.dalc.directional[j].max)) changed = true; + if (DrawColorEdit(std::format("Min##{}", labels[j]), settings.dalc.directional[j].min)) changed = true; + ImGui::Spacing(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } +} + +void LightingTemplateWidget::ApplyChanges() +{ + SetLightingTemplateValues(); + SaveSettings(); +} + +void LightingTemplateWidget::RevertChanges() +{ + LoadLightingTemplateValues(); +} + +void LightingTemplateWidget::SetLightingTemplateValues() +{ + auto& data = lightingTemplate->data; + auto& dalc = lightingTemplate->directionalAmbientLightingColors; + + Float3ToColor(settings.ambient, data.ambient); + Float3ToColor(settings.directional, data.directional); + Float3ToColor(settings.fogColorNear, data.fogColorNear); + Float3ToColor(settings.fogColorFar, data.fogColorFar); + + data.fogNear = settings.fogNear; + data.fogFar = settings.fogFar; + data.directionalXY = static_cast(settings.directionalXY); + data.directionalZ = static_cast(settings.directionalZ); + data.directionalFade = settings.directionalFade; + data.clipDist = settings.clipDist; + data.fogPower = settings.fogPower; + data.fogClamp = settings.fogClamp; + data.lightFadeStart = settings.lightFadeStart; + data.lightFadeEnd = settings.lightFadeEnd; + + dalc.fresnelPower = settings.dalc.fresnelPower; + Float3ToColor(settings.dalc.specular, dalc.specular); + + Float3ToColor(settings.dalc.directional[0].max, dalc.directional.x.max); + Float3ToColor(settings.dalc.directional[0].min, dalc.directional.x.min); + + Float3ToColor(settings.dalc.directional[1].max, dalc.directional.y.max); + Float3ToColor(settings.dalc.directional[1].min, dalc.directional.y.min); + + Float3ToColor(settings.dalc.directional[2].max, dalc.directional.z.max); + Float3ToColor(settings.dalc.directional[2].min, dalc.directional.z.min); +} + +void LightingTemplateWidget::LoadLightingTemplateValues() +{ + auto& data = lightingTemplate->data; + auto& dalc = lightingTemplate->directionalAmbientLightingColors; + + ColorToFloat3(data.ambient, settings.ambient); + ColorToFloat3(data.directional, settings.directional); + ColorToFloat3(data.fogColorNear, settings.fogColorNear); + ColorToFloat3(data.fogColorFar, settings.fogColorFar); + + settings.fogNear = data.fogNear; + settings.fogFar = data.fogFar; + settings.directionalXY = static_cast(data.directionalXY); + settings.directionalZ = static_cast(data.directionalZ); + settings.directionalFade = data.directionalFade; + settings.clipDist = data.clipDist; + settings.fogPower = data.fogPower; + settings.fogClamp = data.fogClamp; + settings.lightFadeStart = data.lightFadeStart; + settings.lightFadeEnd = data.lightFadeEnd; + + settings.dalc.fresnelPower = dalc.fresnelPower; + ColorToFloat3(dalc.specular, settings.dalc.specular); + + ColorToFloat3(dalc.directional.x.max, settings.dalc.directional[0].max); + ColorToFloat3(dalc.directional.x.min, settings.dalc.directional[0].min); + + ColorToFloat3(dalc.directional.y.max, settings.dalc.directional[1].max); + ColorToFloat3(dalc.directional.y.min, settings.dalc.directional[1].min); + + ColorToFloat3(dalc.directional.z.max, settings.dalc.directional[2].max); + ColorToFloat3(dalc.directional.z.min, settings.dalc.directional[2].min); +} + void LightingTemplateWidget::LoadSettings() { + if (!js.empty()) { + settings = js; + } } void LightingTemplateWidget::SaveSettings() { -} \ No newline at end of file + js = settings; +} diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.h b/src/WeatherEditor/Weather/LightingTemplateWidget.h index bca2ed559c..d9e5627701 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.h +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.h @@ -11,11 +11,56 @@ class LightingTemplateWidget : public Widget { form = a_lightingTemplate; lightingTemplate = a_lightingTemplate; + LoadLightingTemplateValues(); } + struct DirectionalColor + { + float3 min; + float3 max; + }; + + struct DALC + { + DirectionalColor directional[3]; + float3 specular; + float fresnelPower; + }; + + struct Settings + { + float3 ambient; + float3 directional; + float3 fogColorNear; + float3 fogColorFar; + float fogNear; + float fogFar; + float directionalXY; + float directionalZ; + float directionalFade; + float clipDist; + float fogPower; + float fogClamp; + float lightFadeStart; + float lightFadeEnd; + DALC dalc; + }; + + Settings settings; + ~LightingTemplateWidget(); virtual void DrawWidget() override; virtual void LoadSettings() override; virtual void SaveSettings() override; + + void SetLightingTemplateValues(); + void LoadLightingTemplateValues(); + void ApplyChanges(); + void RevertChanges(); + +private: + void DrawDALCSettings(); + void DrawBasicSettings(); + void DrawFogSettings(); }; \ No newline at end of file diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 641265f5ea..88e1236688 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -8,6 +8,23 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Atmosphere, colorTimes) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DirectionalColor, max, min) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DALC, specular, fresnelPower, directional) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Cloud, cloudLayerSpeedY, cloudLayerSpeedX, color, cloudAlpha) + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::ImageSpaceSettings, + hdrEyeAdaptSpeed, + hdrBloomBlurRadius, + hdrBloomThreshold, + hdrBloomScale, + hdrSunlightScale, + hdrSkyScale, + cinematicSaturation, + cinematicBrightness, + cinematicContrast, + tintColor, + tintAmount, + dofStrength, + dofDistance, + dofRange) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Settings, parent, @@ -15,9 +32,10 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Settings, weatherProperties, weatherColors, fogProperties, - - weatherColors, + atmosphereColors, dalc, + clouds, + imageSpaces, featureSettings) WeatherWidget::~WeatherWidget() @@ -27,19 +45,21 @@ WeatherWidget::~WeatherWidget() void WeatherWidget::DrawWidget() { ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); - if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_MenuBar)) { - DrawMenu(); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { auto editorWindow = EditorWindow::GetSingleton(); bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; + // Weather lock and time controls (inline) + float buttonWidth = ImGui::GetContentRegionAvail().x * 0.49f; + // Weather lock controls if (isLocked) { - if (ImGui::Button("Unlock Weather", ImVec2(-1, 0))) { + if (ImGui::Button("Unlock Weather", ImVec2(buttonWidth, 0))) { editorWindow->UnlockWeather(); } } else { - if (ImGui::Button("Lock & Force This Weather", ImVec2(-1, 0))) { + if (ImGui::Button("Lock & Force This Weather", ImVec2(buttonWidth, 0))) { editorWindow->LockWeather(weather); } if (ImGui::IsItemHovered()) { @@ -47,6 +67,8 @@ void WeatherWidget::DrawWidget() } } + ImGui::SameLine(); + // Time pause toggle bool isPaused = editorWindow->IsTimePaused(); if (isPaused) { @@ -65,44 +87,78 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Pause/resume time progression in game"); } - // Show Apply/Revert buttons only when auto-apply is disabled - if (!editorWindow->settings.autoApplyChanges) { - auto menu = globals::menu; - bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && - menu->uiIcons.saveSettings.texture && - menu->uiIcons.featureSettingRevert.texture; + auto menu = globals::menu; + bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && + menu->uiIcons.applyToGame.texture && + menu->uiIcons.saveSettings.texture && + menu->uiIcons.loadSettings.texture; - if (useIcons) { - // Icon-based buttons - const float iconSize = ImGui::GetFrameHeight(); - const ImVec2 buttonSize(iconSize, iconSize); + if (useIcons) { + const float iconSize = ImGui::GetFrameHeight(); + const ImVec2 buttonSize(iconSize, iconSize); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); - if (ImGui::ImageButton("##ApplyWeather", menu->uiIcons.saveSettings.texture, buttonSize)) { + // Apply to game button - only show when auto-apply is disabled + if (!editorWindow->settings.autoApplyChanges && menu->uiIcons.applyToGame.texture) { + if (ImGui::ImageButton("##ApplyToGame", menu->uiIcons.applyToGame.texture, buttonSize)) { ApplyChanges(); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Apply changes to the game"); } - ImGui::SameLine(); + } - if (ImGui::ImageButton("##RevertWeather", menu->uiIcons.featureSettingRevert.texture, buttonSize)) { - RevertChanges(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Revert to saved values"); + // Save to file button - always visible + if (ImGui::ImageButton("##SaveWeather", menu->uiIcons.saveSettings.texture, buttonSize)) { + Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save to file"); + } + + ImGui::SameLine(); + + // Load from file button - always visible + if (ImGui::ImageButton("##LoadWeather", menu->uiIcons.loadSettings.texture, buttonSize)) { + Load(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load from file"); + } + + ImGui::SameLine(); + + // Delete button - always visible + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::ImageButton("##DeleteWeather", menu->uiIcons.deleteSettings.texture, buttonSize)) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + ImGui::OpenPopup("ConfirmDelete"); } + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete saved file and revert to defaults"); + } - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); - } else { - // Text-based fallback buttons + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + } else { + // Text-based fallback buttons + bool hasApplyButton = !editorWindow->settings.autoApplyChanges; + int buttonCount = hasApplyButton ? 4 : 3; + float textButtonWidth = ImGui::GetContentRegionAvail().x / buttonCount; + + // Apply button - only show when auto-apply is disabled + if (hasApplyButton) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { + if (ImGui::Button("Apply", ImVec2(textButtonWidth * 0.97f, 0))) { ApplyChanges(); } ImGui::PopStyleColor(); @@ -110,16 +166,72 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Apply changes to the game"); } ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - if (ImGui::Button("Revert", ImVec2(-1, 0))) { - RevertChanges(); - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Revert to saved values"); + } + + // Save button - always visible + if (ImGui::Button("Save", ImVec2(textButtonWidth * 0.97f, 0))) { + Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save to file"); + } + + ImGui::SameLine(); + + // Load button - always visible + if (ImGui::Button("Load", ImVec2(textButtonWidth * 0.97f, 0))) { + Load(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load from file"); + } + + ImGui::SameLine(); + + // Delete button - always visible + float deleteIconSize = ImGui::GetFrameHeight(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::ImageButton("##DeleteWeatherText", menu->uiIcons.deleteSettings.texture, ImVec2(deleteIconSize, deleteIconSize))) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + ImGui::OpenPopup("ConfirmDelete"); } } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete saved file and revert to defaults"); + } } + + // Confirmation popup for delete + if (ImGui::BeginPopupModal("ConfirmDelete", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Are you sure you want to delete this file?"); + ImGui::Text("This will revert to vanilla/mod provided values."); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Checkbox("Don't show this warning again", &editorWindow->settings.suppressDeleteWarning)) { + // Save the preference immediately + editorWindow->Save(); + } + + ImGui::Spacing(); + + if (ImGui::Button("Yes", ImVec2(120, 0))) { + Delete(); + ImGui::CloseCurrentPopup(); + } + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("No", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::Separator(); auto& widgets = editorWindow->weatherWidgets; @@ -204,6 +316,11 @@ void WeatherWidget::DrawWidget() ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("ImageSpace")) { + DrawImageSpaceSettings(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } } @@ -212,8 +329,57 @@ void WeatherWidget::DrawWidget() void WeatherWidget::LoadSettings() { + bool hadErrors = false; + if (!js.empty()) { - settings = js; + try { + // Attempt to load settings from JSON + settings = js; + + // Validate that critical fields were loaded correctly + if (js.contains("weatherProperties") && settings.weatherProperties.empty() && !js["weatherProperties"].empty()) { + logger::warn("Weather {}: weatherProperties loaded but appears empty, reverting to vanilla values", GetEditorID()); + hadErrors = true; + } + if (js.contains("weatherColors") && settings.weatherColors.empty() && !js["weatherColors"].empty()) { + logger::warn("Weather {}: weatherColors loaded but appears empty, reverting to vanilla values", GetEditorID()); + hadErrors = true; + } + if (js.contains("fogProperties") && settings.fogProperties.empty() && !js["fogProperties"].empty()) { + logger::warn("Weather {}: fogProperties loaded but appears empty, reverting to vanilla values", GetEditorID()); + hadErrors = true; + } + + if (hadErrors) { + // Fallback to vanilla/game values + LoadWeatherValues(); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Some values failed to load for {}", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f), + 3.0f); + } else { + logger::info("Weather {}: Loaded settings - {} weather properties, {} colors, {} fog properties", + GetEditorID(), + settings.weatherProperties.size(), + settings.weatherColors.size(), + settings.fogProperties.size()); + } + + } catch (const nlohmann::json::exception& e) { + logger::error("Weather {}: Failed to deserialize settings from JSON: {}", GetEditorID(), e.what()); + logger::error("JSON content: {}", js.dump(2)); + // Fallback to vanilla/game values on exception + LoadWeatherValues(); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Some values failed to load for {}", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f), + 3.0f); + return; + } + } else { + // No JSON data, load vanilla values + logger::info("Weather {}: No JSON data, loading vanilla values", GetEditorID()); + LoadWeatherValues(); } LoadFeatureSettings(); } @@ -221,7 +387,31 @@ void WeatherWidget::LoadSettings() void WeatherWidget::SaveSettings() { SaveFeatureSettings(); - js = settings; + + try { + js = settings; + + // Log what we're saving for debugging + logger::info("Weather {}: Saving settings - {} weather properties, {} colors, {} fog properties", + GetEditorID(), + settings.weatherProperties.size(), + settings.weatherColors.size(), + settings.fogProperties.size()); + + // Validate serialization worked + if (js.is_null()) { + logger::error("Weather {}: Serialization produced null JSON!", GetEditorID()); + } else if (!js.contains("weatherProperties")) { + logger::error("Weather {}: Serialized JSON missing weatherProperties field!", GetEditorID()); + } else if (!js.contains("atmosphereColors")) { + logger::error("Weather {}: Serialized JSON missing atmosphereColors field!", GetEditorID()); + } else if (!js.contains("clouds")) { + logger::error("Weather {}: Serialized JSON missing clouds field!", GetEditorID()); + } + + } catch (const nlohmann::json::exception& e) { + logger::error("Weather {}: Failed to serialize settings to JSON: {}", GetEditorID(), e.what()); + } } WeatherWidget* WeatherWidget::GetParent() @@ -327,6 +517,9 @@ void WeatherWidget::SetWeatherValues() Float3ToColor(settingsCloud.color[j], cloudColors[j]); } } + + // ImageSpace + SetImageSpaceValues(); } void WeatherWidget::LoadWeatherValues() @@ -415,153 +608,148 @@ void WeatherWidget::LoadWeatherValues() ColorToFloat3(cloudColors[j], settingsCloud.color[j]); } } + + // ImageSpace + LoadImageSpaceValues(); } void WeatherWidget::DrawDALCSettings() { - if (ImGui::CollapsingHeader("DALC settings", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - bool& doesInherit = settings.inheritance["DALC"]; - ImGui::Checkbox("Inherit From Parent##dalc", &doesInherit); + bool& doesInherit = settings.inheritance["DALC"]; + ImGui::Checkbox("Inherit From Parent##dalc", &doesInherit); - if (doesInherit && HasParent()) { - for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { - settings.dalc[i] = GetParent()->settings.dalc[i]; - } - } else { - doesInherit = false; - bool changed = false; - for (int i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { - std::string label = ColorTimeLabel(i); + if (doesInherit && HasParent()) { + for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { + settings.dalc[i] = GetParent()->settings.dalc[i]; + } + } else { + doesInherit = false; + bool changed = false; + for (int i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { + std::string label = ColorTimeLabel(i); - if (ImGui::CollapsingHeader(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - ImGui::Spacing(); - if (DrawColorEdit(std::format("Specular##{}", label), settings.dalc[i].specular)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat(std::format("Fresnel Power##{}", label), settings.dalc[i].fresnelPower)) changed = true; - ImGui::Spacing(); + if (ImGui::CollapsingHeader(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + ImGui::Spacing(); + if (DrawColorEdit(std::format("Specular##{}", label), settings.dalc[i].specular)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat(std::format("Fresnel Power##{}", label), settings.dalc[i].fresnelPower)) changed = true; + ImGui::Spacing(); - for (int j = 0; j < 3; j++) { - if (DrawColorEdit(std::format("DALC X Max##{}", label), settings.dalc[i].directional[j].max)) changed = true; - if (DrawColorEdit(std::format("DALC X Min##{}", label), settings.dalc[i].directional[j].min)) changed = true; - ImGui::Spacing(); - } + for (int j = 0; j < 3; j++) { + if (DrawColorEdit(std::format("DALC X Max##{}", label), settings.dalc[i].directional[j].max)) changed = true; + if (DrawColorEdit(std::format("DALC X Min##{}", label), settings.dalc[i].directional[j].min)) changed = true; + ImGui::Spacing(); } } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } + } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } } void WeatherWidget::DrawWeatherColorSettings() { - if (ImGui::CollapsingHeader("Atmosphere Colors", ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - bool& doesInherit = settings.inheritance["Atmosphere Colors"]; - ImGui::Checkbox("Inherit From Parent##atmosphereColors", &doesInherit); + bool& doesInherit = settings.inheritance["Atmosphere Colors"]; + ImGui::Checkbox("Inherit From Parent##atmosphereColors", &doesInherit); - if (&doesInherit && HasParent()) { - for (size_t i = 0; i < ColorTypes::kTotal; i++) { - settings.atmosphereColors[i] = GetParent()->settings.atmosphereColors[i]; - } - } else { - doesInherit = false; - bool changed = false; + if (&doesInherit && HasParent()) { + for (size_t i = 0; i < ColorTypes::kTotal; i++) { + settings.atmosphereColors[i] = GetParent()->settings.atmosphereColors[i]; + } + } else { + doesInherit = false; + bool changed = false; - for (int i = 0; i < ColorTypes::kTotal; i++) { - std::string colorTypeLabel = ColorTypeLabel(i); + for (int i = 0; i < ColorTypes::kTotal; i++) { + std::string colorTypeLabel = ColorTypeLabel(i); - if (ImGui::CollapsingHeader(colorTypeLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - for (int j = 0; j < ColorTimes::kTotal; j++) { - if (DrawColorEdit(std::format("{}##{}", ColorTimeLabel(j), colorTypeLabel), settings.atmosphereColors[i].colorTimes[j])) changed = true; - ImGui::Spacing(); - } + if (ImGui::CollapsingHeader(colorTypeLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + for (int j = 0; j < ColorTimes::kTotal; j++) { + if (DrawColorEdit(std::format("{}##{}", ColorTimeLabel(j), colorTypeLabel), settings.atmosphereColors[i].colorTimes[j])) changed = true; + ImGui::Spacing(); } } + } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } } void WeatherWidget::DrawCloudSettings() { - if (ImGui::CollapsingHeader("Clouds Properties", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - bool& doesInherit = settings.inheritance["Clouds"]; - ImGui::Checkbox("Inherit From Parent##cloud", &doesInherit); + bool& doesInherit = settings.inheritance["Clouds"]; + ImGui::Checkbox("Inherit From Parent##cloud", &doesInherit); - if (doesInherit && HasParent()) { - for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { - settings.dalc[i] = GetParent()->settings.dalc[i]; - } - } else { - doesInherit = false; - bool changed = false; - for (int i = 0; i < TESWeather::kTotalLayers; i++) { - std::string layer = std::format("Layer {}", i); - - if (ImGui::CollapsingHeader(layer.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; - ImGui::Spacing(); - if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; + if (doesInherit && HasParent()) { + for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { + settings.dalc[i] = GetParent()->settings.dalc[i]; + } + } else { + doesInherit = false; + bool changed = false; + for (int i = 0; i < TESWeather::kTotalLayers; i++) { + std::string layer = std::format("Layer {}", i); + + if (ImGui::CollapsingHeader(layer.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; + ImGui::Spacing(); + if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; - for (int j = 0; j < ColorTimes::kTotal; j++) { - std::string colorTime = ColorTimeLabel(j).c_str(); + for (int j = 0; j < ColorTimes::kTotal; j++) { + std::string colorTime = ColorTimeLabel(j).c_str(); - if (ImGui::CollapsingHeader(colorTime.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - if (DrawColorEdit(std::format("Cloud Color##{}{}", colorTime, i), settings.clouds[i].color[j])) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat(std::format("Cloud Alpha##{}{}", colorTime, i), settings.clouds[i].cloudAlpha[j])) changed = true; - ImGui::Spacing(); - } + if (ImGui::CollapsingHeader(colorTime.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { + if (DrawColorEdit(std::format("Cloud Color##{}{}", colorTime, i), settings.clouds[i].color[j])) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat(std::format("Cloud Alpha##{}{}", colorTime, i), settings.clouds[i].cloudAlpha[j])) changed = true; + ImGui::Spacing(); } } } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } + } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } } void WeatherWidget::DrawProperties(std::string category, std::map properties) { - if (ImGui::CollapsingHeader(std::format("{} Properties", category).c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - bool& doesInherit = settings.inheritance[category]; - ImGui::Checkbox(std::format("Inherit From Parent##{}", category).c_str(), &doesInherit); + bool& doesInherit = settings.inheritance[category]; + ImGui::Checkbox(std::format("Inherit From Parent##{}", category).c_str(), &doesInherit); - if (doesInherit && HasParent()) { - for (auto& p : properties) { - InheritFromParent(p.first); - } - } else { - doesInherit = false; - bool changed = false; - - for (auto& p : properties) { - switch (p.second) { - case 0: - if (DrawSliderInt8(p.first, settings.weatherProperties[p.first])) changed = true; - break; - case 1: - if (DrawColorEdit(p.first, settings.weatherColors[p.first])) changed = true; - break; - case 2: - if (DrawSliderUint8(p.first, settings.weatherProperties[p.first])) changed = true; - break; - case 3: - if (DrawSliderFloat(p.first, settings.fogProperties[p.first])) changed = true; - break; - default: - break; - } + if (doesInherit && HasParent()) { + for (auto& p : properties) { + InheritFromParent(p.first); + } + } else { + doesInherit = false; + bool changed = false; + + for (auto& p : properties) { + switch (p.second) { + case 0: + if (DrawSliderInt8(p.first, settings.weatherProperties[p.first])) changed = true; + break; + case 1: + if (DrawColorEdit(p.first, settings.weatherColors[p.first])) changed = true; + break; + case 2: + if (DrawSliderUint8(p.first, settings.weatherProperties[p.first])) changed = true; + break; + case 3: + if (DrawSliderFloat(p.first, settings.fogProperties[p.first])) changed = true; + break; + default: + break; } + } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } @@ -582,7 +770,6 @@ void WeatherWidget::InheritFromParent(const std::string& property) void WeatherWidget::SaveFeatureSettings() { auto* weatherManager = WeatherManager::GetSingleton(); - auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); for (const auto& [featureName, featureJson] : settings.featureSettings) { if (!featureJson.empty()) { @@ -629,62 +816,234 @@ void WeatherWidget::RevertChanges() void WeatherWidget::DrawFeatureSettings() { - if (ImGui::CollapsingHeader("Per-Feature Weather Settings", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - ImGui::TextWrapped("Configure feature-specific settings that will be applied when this weather is active."); - ImGui::Spacing(); + ImGui::TextWrapped("Configure feature-specific settings that will be applied when this weather is active."); + ImGui::Spacing(); - auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); + auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); - for (auto* feature : Feature::GetFeatureList()) { - if (!feature || !feature->loaded) { - continue; + for (auto* feature : Feature::GetFeatureList()) { + if (!feature || !feature->loaded) { + continue; + } + + std::string featureName = feature->GetShortName(); + + // Check if feature has registered weather variables + if (!globalRegistry->HasWeatherSupport(featureName)) { + continue; + } + + std::string displayName = feature->GetName(); + + if (ImGui::TreeNode(displayName.c_str())) { + ImGui::Text("Feature: %s", featureName.c_str()); + ImGui::Spacing(); + + // Show if settings exist for this feature + bool hasSettings = settings.featureSettings.find(featureName) != settings.featureSettings.end() && + !settings.featureSettings[featureName].empty(); + + if (hasSettings) { + ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); + + if (ImGui::Button("Clear Settings")) { + settings.featureSettings[featureName] = json::object(); + } + ImGui::SameLine(); + if (ImGui::Button("View JSON")) { + ImGui::OpenPopup("FeatureJSON"); + } + + if (ImGui::BeginPopup("FeatureJSON")) { + ImGui::Text("Settings JSON:"); + ImGui::Separator(); + std::string jsonStr = settings.featureSettings[featureName].dump(2); + ImGui::TextWrapped("%s", jsonStr.c_str()); + ImGui::EndPopup(); + } + } else { + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "No weather-specific settings"); } - std::string featureName = feature->GetShortName(); - - // Check if feature has registered weather variables - if (!globalRegistry->HasWeatherSupport(featureName)) { - continue; + ImGui::Spacing(); + ImGui::TextWrapped("Note: Feature settings should be configured through the feature's own settings panel. " + "This section shows which features have per-weather overrides."); + + ImGui::TreePop(); + } + } +} + +void WeatherWidget::DrawImageSpaceSettings() +{ + if (!weather) { + ImGui::TextColored({ 1.0f, 0.0f, 0.0f, 1.0f }, "Weather object is null!"); + return; + } + + ImGui::TextWrapped("Configure ImageSpace (post-processing) effects for different times of day."); + ImGui::Spacing(); + ImGui::Separator(); + + const char* timeNames[] = { "Sunrise", "Day", "Sunset", "Night" }; + + for (int timeIdx = 0; timeIdx < ColorTimes::kTotal; timeIdx++) { + auto& imgSpace = settings.imageSpaces[timeIdx]; + RE::TESImageSpace* weatherImgSpace = weather->imageSpaces[timeIdx]; + + ImGui::PushID(timeIdx); + + if (ImGui::CollapsingHeader(timeNames[timeIdx], ImGuiTreeNodeFlags_DefaultOpen)) { + // Show which ImageSpace form is assigned + if (weatherImgSpace) { + ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "ImageSpace: %s (%08X)", + weatherImgSpace->GetFormEditorID(), + weatherImgSpace->GetFormID()); + } else { + ImGui::TextColored({ 1.0f, 0.5f, 0.0f, 1.0f }, "No ImageSpace assigned"); } - std::string displayName = feature->GetName(); + ImGui::Spacing(); - if (ImGui::TreeNode(displayName.c_str())) { - ImGui::Text("Feature: %s", featureName.c_str()); - ImGui::Spacing(); + // HDR Settings + if (ImGui::TreeNode("HDR Settings")) { + bool changed = false; + changed |= ImGui::SliderFloat("Eye Adapt Speed", &imgSpace.hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f"); + changed |= ImGui::SliderFloat("Bloom Blur Radius", &imgSpace.hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f"); + changed |= ImGui::SliderFloat("Bloom Threshold", &imgSpace.hdrBloomThreshold, 0.0f, 10.0f, "%.3f"); + changed |= ImGui::SliderFloat("Bloom Scale", &imgSpace.hdrBloomScale, 0.0f, 10.0f, "%.3f"); + changed |= ImGui::SliderFloat("Sunlight Scale", &imgSpace.hdrSunlightScale, 0.0f, 10.0f, "%.3f"); + changed |= ImGui::SliderFloat("Sky Scale", &imgSpace.hdrSkyScale, 0.0f, 10.0f, "%.3f"); - // Show if settings exist for this feature - bool hasSettings = settings.featureSettings.find(featureName) != settings.featureSettings.end() && - !settings.featureSettings[featureName].empty(); + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + SetImageSpaceValues(); + } - if (hasSettings) { - ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); - - if (ImGui::Button("Clear Settings")) { - settings.featureSettings[featureName] = json::object(); - } - ImGui::SameLine(); - if (ImGui::Button("View JSON")) { - ImGui::OpenPopup("FeatureJSON"); - } + ImGui::TreePop(); + } - if (ImGui::BeginPopup("FeatureJSON")) { - ImGui::Text("Settings JSON:"); - ImGui::Separator(); - std::string jsonStr = settings.featureSettings[featureName].dump(2); - ImGui::TextWrapped("%s", jsonStr.c_str()); - ImGui::EndPopup(); - } - } else { - ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "No weather-specific settings"); + // Cinematic Settings + if (ImGui::TreeNode("Cinematic Settings")) { + bool changed = false; + changed |= ImGui::SliderFloat("Saturation", &imgSpace.cinematicSaturation, 0.0f, 2.0f, "%.3f"); + changed |= ImGui::SliderFloat("Brightness", &imgSpace.cinematicBrightness, 0.0f, 2.0f, "%.3f"); + changed |= ImGui::SliderFloat("Contrast", &imgSpace.cinematicContrast, 0.0f, 2.0f, "%.3f"); + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + SetImageSpaceValues(); } - ImGui::Spacing(); - ImGui::TextWrapped("Note: Feature settings should be configured through the feature's own settings panel. " - "This section shows which features have per-weather overrides."); + ImGui::TreePop(); + } + + // Tint Settings + if (ImGui::TreeNode("Tint Settings")) { + bool changed = false; + changed |= ImGui::ColorEdit3("Tint Color", &imgSpace.tintColor.x); + changed |= ImGui::SliderFloat("Tint Amount", &imgSpace.tintAmount, 0.0f, 1.0f, "%.3f"); + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + SetImageSpaceValues(); + } + + ImGui::TreePop(); + } + + // Depth of Field Settings + if (ImGui::TreeNode("Depth of Field")) { + bool changed = false; + changed |= ImGui::SliderFloat("DOF Strength", &imgSpace.dofStrength, 0.0f, 10.0f, "%.3f"); + changed |= ImGui::SliderFloat("DOF Distance", &imgSpace.dofDistance, 0.0f, 10000.0f, "%.1f"); + changed |= ImGui::SliderFloat("DOF Range", &imgSpace.dofRange, 0.0f, 10000.0f, "%.1f"); + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + SetImageSpaceValues(); + } ImGui::TreePop(); } + + ImGui::Spacing(); } + + ImGui::PopID(); + } +} + +void WeatherWidget::LoadImageSpaceValues() +{ + if (!weather) + return; + + for (int timeIdx = 0; timeIdx < ColorTimes::kTotal; timeIdx++) { + RE::TESImageSpace* imageSpace = weather->imageSpaces[timeIdx]; + if (!imageSpace) + continue; + + auto& imgSettings = settings.imageSpaces[timeIdx]; + auto& data = imageSpace->data; + + // HDR + imgSettings.hdrEyeAdaptSpeed = data.hdr.eyeAdaptSpeed; + imgSettings.hdrBloomBlurRadius = data.hdr.bloomBlurRadius; + imgSettings.hdrBloomThreshold = data.hdr.bloomThreshold; + imgSettings.hdrBloomScale = data.hdr.bloomScale; + imgSettings.hdrSunlightScale = data.hdr.sunlightScale; + imgSettings.hdrSkyScale = data.hdr.skyScale; + + // Cinematic + imgSettings.cinematicSaturation = data.cinematic.saturation; + imgSettings.cinematicBrightness = data.cinematic.brightness; + imgSettings.cinematicContrast = data.cinematic.contrast; + + // Tint + imgSettings.tintColor.x = data.tint.color.red; + imgSettings.tintColor.y = data.tint.color.green; + imgSettings.tintColor.z = data.tint.color.blue; + imgSettings.tintAmount = data.tint.amount; + + // Depth of Field + imgSettings.dofStrength = data.depthOfField.strength; + imgSettings.dofDistance = data.depthOfField.distance; + imgSettings.dofRange = data.depthOfField.range; + } +} + +void WeatherWidget::SetImageSpaceValues() +{ + if (!weather) + return; + + for (int timeIdx = 0; timeIdx < ColorTimes::kTotal; timeIdx++) { + RE::TESImageSpace* imageSpace = weather->imageSpaces[timeIdx]; + if (!imageSpace) + continue; + + auto& imgSettings = settings.imageSpaces[timeIdx]; + auto& data = imageSpace->data; + + // HDR + data.hdr.eyeAdaptSpeed = imgSettings.hdrEyeAdaptSpeed; + data.hdr.bloomBlurRadius = imgSettings.hdrBloomBlurRadius; + data.hdr.bloomThreshold = imgSettings.hdrBloomThreshold; + data.hdr.bloomScale = imgSettings.hdrBloomScale; + data.hdr.sunlightScale = imgSettings.hdrSunlightScale; + data.hdr.skyScale = imgSettings.hdrSkyScale; + + // Cinematic + data.cinematic.saturation = imgSettings.cinematicSaturation; + data.cinematic.brightness = imgSettings.cinematicBrightness; + data.cinematic.contrast = imgSettings.cinematicContrast; + + // Tint + data.tint.color.red = imgSettings.tintColor.x; + data.tint.color.green = imgSettings.tintColor.y; + data.tint.color.blue = imgSettings.tintColor.z; + data.tint.amount = imgSettings.tintAmount; + + // Depth of Field + data.depthOfField.strength = imgSettings.dofStrength; + data.depthOfField.distance = imgSettings.dofDistance; + data.depthOfField.range = imgSettings.dofRange; } } diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h index d18611169f..c0ece8f4cc 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.h +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -46,6 +46,31 @@ class WeatherWidget : public Widget float cloudAlpha[ColorTimes::kTotal]; }; + struct ImageSpaceSettings + { + // HDR Settings + float hdrEyeAdaptSpeed = 0.0f; + float hdrBloomBlurRadius = 0.0f; + float hdrBloomThreshold = 0.0f; + float hdrBloomScale = 0.0f; + float hdrSunlightScale = 0.0f; + float hdrSkyScale = 0.0f; + + // Cinematic Settings + float cinematicSaturation = 0.0f; + float cinematicBrightness = 0.0f; + float cinematicContrast = 0.0f; + + // Tint Colors + float3 tintColor = { 1.0f, 1.0f, 1.0f }; + float tintAmount = 0.0f; + + // Depth of Field + float dofStrength = 0.0f; + float dofDistance = 0.0f; + float dofRange = 0.0f; + }; + struct Settings { std::string parent = "None"; @@ -58,6 +83,9 @@ class WeatherWidget : public Widget DALC dalc[ColorTimes::kTotal]; Cloud clouds[TESWeather::kTotalLayers]; + // ImageSpace settings for each time of day + ImageSpaceSettings imageSpaces[ColorTimes::kTotal]; + // Per-feature settings storage std::map featureSettings; }; @@ -81,11 +109,16 @@ class WeatherWidget : public Widget void SaveFeatureSettings(); void LoadFeatureSettings(); + // ImageSpace methods + void LoadImageSpaceValues(); + void SetImageSpaceValues(); + private: void DrawDALCSettings(); void DrawWeatherColorSettings(); void DrawCloudSettings(); void DrawFeatureSettings(); + void DrawImageSpaceSettings(); void DrawProperties(std::string category, std::map properties); void InheritFromParent(const std::string& property); }; \ No newline at end of file diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp index fbe4469222..d308c74662 100644 --- a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp @@ -1,6 +1,6 @@ #include "WorldSpaceWidget.h" -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(WorldSpaceWidget::Settings, temp) +#include "../EditorWindow.h" WorldSpaceWidget::~WorldSpaceWidget() { @@ -8,20 +8,85 @@ WorldSpaceWidget::~WorldSpaceWidget() void WorldSpaceWidget::DrawWidget() { + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) { DrawMenu(); + + auto editorWindow = EditorWindow::GetSingleton(); + + if (!editorWindow->settings.autoApplyChanges) { + auto menu = globals::menu; + bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && + menu->uiIcons.saveSettings.texture && + menu->uiIcons.featureSettingRevert.texture; + + if (useIcons) { + const float iconSize = ImGui::GetFrameHeight(); + const ImVec2 buttonSize(iconSize, iconSize); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + + if (ImGui::ImageButton("##ApplyWorldSpace", menu->uiIcons.saveSettings.texture, buttonSize)) { + ApplyChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + + ImGui::SameLine(); + + if (ImGui::ImageButton("##RevertWorldSpace", menu->uiIcons.featureSettingRevert.texture, buttonSize)) { + RevertChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } + + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); + if (ImGui::Button("Apply", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { + ApplyChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + if (ImGui::Button("Revert", ImVec2(-1, 0))) { + RevertChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } + } + } + ImGui::Separator(); } ImGui::End(); } +void WorldSpaceWidget::ApplyChanges() +{ + SaveSettings(); +} + +void WorldSpaceWidget::RevertChanges() +{ + LoadSettings(); +} + void WorldSpaceWidget::LoadSettings() { - if (!js.empty()) { - settings = js; - } + // Empty - no settings to load } void WorldSpaceWidget::SaveSettings() { - js = settings; + // Empty - no settings to save } diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.h b/src/WeatherEditor/Weather/WorldSpaceWidget.h index a336f6fa6e..6305d4beb6 100644 --- a/src/WeatherEditor/Weather/WorldSpaceWidget.h +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.h @@ -19,9 +19,11 @@ class WorldSpaceWidget : public Widget virtual void LoadSettings() override; virtual void SaveSettings() override; + void ApplyChanges(); + void RevertChanges(); + struct Settings { - int temp; // Temp var to resolve macro issue in cpp file as worldspace has no settings at the moment. Can be removed once settings are added. }; Settings settings; diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h index 80f5381d12..3582a8310d 100644 --- a/src/WeatherEditor/WeatherUtils.h +++ b/src/WeatherEditor/WeatherUtils.h @@ -1,3 +1,5 @@ +#pragma once + #include "Util.h" void Float3ToColor(const float3& newColor, RE::Color& color); diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index 619a3ad151..e8ef3309fa 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -1,4 +1,5 @@ #include "Widget.h" +#include "EditorWindow.h" #include "State.h" #include "Util.h" @@ -29,14 +30,34 @@ void Widget::Save() return; } - logger::info("Saving settings file: {}", file); - try { - settingsFile << js.dump(1); + // Validate that we have valid JSON to write + if (js.is_null()) { + logger::warn("{}: Cannot save - JSON data is null", GetEditorID()); + settingsFile.close(); + return; + } + + logger::info("{}: Saving settings file: {}", GetEditorID(), file); + + // Write with indentation for readability + settingsFile << js.dump(2); + settingsFile.flush(); + + if (settingsFile.fail()) { + logger::error("{}: Failed to write settings to file", GetEditorID()); + settingsFile.close(); + return; + } settingsFile.close(); - } catch (const nlohmann::json::parse_error& e) { - logger::warn("Error parsing settings for settings file ({}) : {}\n", filePath, e.what()); + logger::info("{}: Successfully saved settings", GetEditorID()); + + } catch (const nlohmann::json::exception& e) { + logger::error("{}: JSON error while saving settings: {}", GetEditorID(), e.what()); + settingsFile.close(); + } catch (const std::exception& e) { + logger::error("{}: Unexpected error saving settings file: {}", GetEditorID(), e.what()); settingsFile.close(); } } @@ -48,7 +69,10 @@ void Widget::Load() std::ifstream settingsFile(filePath); if (!std::filesystem::exists(filePath)) { - // Does not have any settings so just return. + // No saved file exists, reload vanilla/default values + logger::info("{}: No settings file found, reloading vanilla values", GetEditorID()); + js = json(); + LoadSettings(); return; } @@ -58,15 +82,70 @@ void Widget::Load() } try { - js << settingsFile; + settingsFile >> js; settingsFile.close(); + + // Validate that we loaded valid JSON + if (js.is_null()) { + logger::warn("{}: Loaded JSON is null, file may be empty or invalid", filePath); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Failed to load settings for {}", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f), + 3.0f); + return; + } + + logger::info("{}: Successfully loaded settings file", GetEditorID()); + } catch (const nlohmann::json::parse_error& e) { - logger::warn("Error parsing settings for file ({}) : {}\n", filePath, e.what()); + logger::error("Error parsing settings for file ({}) : {}\n", filePath, e.what()); + logger::error("Parse error at byte {}: {}", e.byte, e.what()); settingsFile.close(); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Failed to parse settings for {}", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f), + 3.0f); + return; + } catch (const std::exception& e) { + logger::error("Unexpected error loading settings file ({}) : {}\n", filePath, e.what()); + settingsFile.close(); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Failed to load settings for {}", GetEditorID()), + ImVec4(1.0f, 0.0f, 0.0f, 1.0f), + 3.0f); + return; } LoadSettings(); } +void Widget::Delete() +{ + std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName(), GetEditorID()); + + if (!std::filesystem::exists(filePath)) { + logger::info("Settings file does not exist, nothing to delete: {}", filePath); + return; + } + + try { + std::filesystem::remove(filePath); + logger::info("Deleted settings file: {}", filePath); + + // Clear the in-memory JSON data + js = json(); + + // Reload settings from vanilla/mod defaults + LoadSettings(); + + EditorWindow::GetSingleton()->ShowNotification( + std::format("Deleted {} - reverted to vanilla values", GetEditorID()), + ImVec4(0.0f, 1.0f, 0.0f, 1.0f), + 3.0f); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Error deleting settings file ({}) : {}\n", filePath, e.what()); + } +} + void Widget::DrawMenu() { if (ImGui::BeginMenuBar()) { @@ -77,6 +156,45 @@ void Widget::DrawMenu() if (ImGui::MenuItem("Load")) { Load(); } + if (ImGui::MenuItem("Revert to Defaults")) { + auto& settings = EditorWindow::GetSingleton()->settings; + if (settings.suppressDeleteWarning) { + // Delete directly if warning is suppressed + Delete(); + } else { + // Open confirmation popup + ImGui::OpenPopup("ConfirmDelete"); + } + } + + // Confirmation popup for delete + if (ImGui::BeginPopupModal("ConfirmDelete", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Are you sure you want to delete this file?"); + ImGui::Text("This will revert to vanilla/mod provided values."); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + auto& settings = EditorWindow::GetSingleton()->settings; + if (ImGui::Checkbox("Don't show this warning again", &settings.suppressDeleteWarning)) { + // Save the preference immediately + EditorWindow::GetSingleton()->Save(); + } + + ImGui::Spacing(); + + if (ImGui::Button("Yes", ImVec2(120, 0))) { + Delete(); + ImGui::CloseCurrentPopup(); + } + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("No", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + ImGui::EndMenu(); } ImGui::EndMenuBar(); diff --git a/src/WeatherEditor/Widget.h b/src/WeatherEditor/Widget.h index 3bdad93b39..cb478471ea 100644 --- a/src/WeatherEditor/Widget.h +++ b/src/WeatherEditor/Widget.h @@ -61,6 +61,7 @@ class Widget void Save(); void Load(); + void Delete(); virtual void LoadSettings() = 0; virtual void SaveSettings() = 0; From f93c395c0cfe22922618c2abe23b6c8eea72d0bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:39:40 +0000 Subject: [PATCH 04/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../WeatherVariableRegistration.md | 63 ++-- .../examples/ExampleWeatherFeature.cpp | 30 +- src/Util.h | 2 +- src/WeatherEditor/EditorWindow.cpp | 291 +++++++++--------- .../Weather/ImageSpaceWidget.cpp | 2 +- .../Weather/LightingTemplateWidget.cpp | 54 ++-- src/WeatherEditor/Weather/WeatherWidget.cpp | 95 +++--- src/WeatherEditor/Widget.cpp | 26 +- 8 files changed, 306 insertions(+), 257 deletions(-) diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md index d4fa81cb90..f4aab35fbb 100644 --- a/docs/weather-system-docs/WeatherVariableRegistration.md +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -10,11 +10,11 @@ The weather variable registration system provides a centralized way for features **WeatherVariableRegistry.h** contains: -- **`IWeatherVariable`**: Base interface for all weather variables -- **`WeatherVariable`**: Templated variable with type-safe serialization and interpolation -- **`FloatVariable`, `Float3Variable`, `Float4Variable`**: Specialized types with range support -- **`FeatureWeatherRegistry`**: Manages all variables for a single feature -- **`GlobalWeatherRegistry`**: Singleton coordinating all features +- **`IWeatherVariable`**: Base interface for all weather variables +- **`WeatherVariable`**: Templated variable with type-safe serialization and interpolation +- **`FloatVariable`, `Float3Variable`, `Float4Variable`**: Specialized types with range support +- **`FeatureWeatherRegistry`**: Manages all variables for a single feature +- **`GlobalWeatherRegistry`**: Singleton coordinating all features ### Data Flow @@ -98,10 +98,11 @@ void MyFeature::RegisterWeatherVariables() override #### Step 3: Implementation Complete The system now automatically: -- Saves/loads weather-specific settings to JSON -- Interpolates variables during weather transitions -- Appears in the weather editor UI -- Handles default values and missing dataanced Usage + +- Saves/loads weather-specific settings to JSON +- Interpolates variables during weather transitions +- Appears in the weather editor UI +- Handles default values and missing dataanced Usage ### Custom Variable Types @@ -144,9 +145,9 @@ LoadSettingsFromWeather(lastWeather, featureName, nextWeatherSettings); // Interpolation during weather transitions globalRegistry->UpdateFeatureFromWeathers( - featureName, - currWeatherSettings, - nextWeatherSettings, + featureName, + currWeatherSettings, + nextWeatherSettings, lerpFactor // 0.0 to 1.0 ); ``` @@ -154,12 +155,14 @@ globalRegistry->UpdateFeatureFromWeathers( ### File Structure Weather-specific settings are stored in: + ``` Data/SKSE/Plugins/CommunityShaders/Weathers/ WeatherEditorID_FormID.json ``` Each file contains settings for all features: + ```json { "FeatureName1": { @@ -218,30 +221,37 @@ if (registry) { ## Implementation Notes ### Memory Management -- Registry uses `std::shared_ptr` for variable lifetime -- Variables store raw pointers to feature data (safe as features outlive registry) -- No copying - variables are modified in-place + +- Registry uses `std::shared_ptr` for variable lifetime +- Variables store raw pointers to feature data (safe as features outlive registry) +- No copying - variables are modified in-place ### Thread Safety + Current implementation is single-threaded (main game thread). Variables are accessed and modified on the same thread that updates weather. ### JSON Serialization + Uses nlohmann::json for type conversion. Built-in support for: -- Primitive types (float, int, bool) -- float2, float3, float4 (see `Utils/Serialize.h`) -- Custom types require NLOHMANN_DEFINE_TYPE_* macros + +- Primitive types (float, int, bool) +- float2, float3, float4 (see `Utils/Serialize.h`) +- Custom types require NLOHMANN*DEFINE_TYPE*\* macros ### Error Handling -- Missing JSON keys use default values -- Type mismatches caught by json exceptions -- Invalid weather files logged but don't crashherVariables::FloatVariable>( - "intensity", "Intensity", "Effect intensity", - &settings.intensity, 1.0f, 0.0f, 2.0f - )); + +- Missing JSON keys use default values +- Type mismatches caught by json exceptions +- Invalid weather files logged but don't crashherVariables::FloatVariable>( + "intensity", "Intensity", "Effect intensity", + &settings.intensity, 1.0f, 0.0f, 2.0f + )); } - // That's it! No save/load/update code needed for weather variables -}; + // That's it! No save/load/update code needed for weather variables + + }; + ``` ## Architecture Benefits @@ -263,3 +273,4 @@ The centralized registry enables: - Variables are directly modified in place (no copying) - Interpolation only happens during weather transitions - Registration is one-time during feature initialization +``` diff --git a/docs/weather-system-docs/examples/ExampleWeatherFeature.cpp b/docs/weather-system-docs/examples/ExampleWeatherFeature.cpp index b9a251c23e..1f7ae87051 100644 --- a/docs/weather-system-docs/examples/ExampleWeatherFeature.cpp +++ b/docs/weather-system-docs/examples/ExampleWeatherFeature.cpp @@ -1,5 +1,5 @@ // Example Feature: Demonstrates the Weather Variable Registration Pattern -// +// // This is a minimal example showing how features can support per-weather settings. // Simply register your variables once during RegisterWeatherVariables() and the weather system handles the rest. @@ -10,17 +10,17 @@ void ExampleWeatherFeature::RegisterWeatherVariables() { // Get or create the registry for this feature auto* registry = WeatherVariables::GlobalWeatherRegistry::GetSingleton() - ->GetOrCreateFeatureRegistry(GetShortName()); + ->GetOrCreateFeatureRegistry(GetShortName()); // Register a simple float variable registry->RegisterVariable(std::make_shared( - "Intensity", // JSON key name - "Effect Intensity", // Display name for UI + "Intensity", // JSON key name + "Effect Intensity", // Display name for UI "Controls how strong the effect is", // Tooltip/description - &settings.intensity, // Pointer to the actual variable - 1.0f, // Default value - 0.0f, 2.0f // Min/max range (optional) - )); + &settings.intensity, // Pointer to the actual variable + 1.0f, // Default value + 0.0f, 2.0f // Min/max range (optional) + )); // Register a float3 color variable registry->RegisterVariable(std::make_shared( @@ -29,7 +29,7 @@ void ExampleWeatherFeature::RegisterWeatherVariables() "RGB color values for the effect", &settings.color, float3{ 1.0f, 0.5f, 0.2f } // Default orange color - )); + )); // Register a float4 variable (could be RGBA color or other 4-component data) registry->RegisterVariable(std::make_shared( @@ -38,7 +38,7 @@ void ExampleWeatherFeature::RegisterWeatherVariables() "RGBA tint color with alpha", &settings.tintColor, float4{ 1.0f, 1.0f, 1.0f, 1.0f } // Default white with full opacity - )); + )); // Register a custom type with manual lerp function // This example shows a bool that transitions at the halfway point @@ -51,8 +51,7 @@ void ExampleWeatherFeature::RegisterWeatherVariables() [](const bool& from, const bool& to, float factor) { // Custom lerp: switch to target value when more than halfway through transition return factor > 0.5f ? to : from; - } - )); + })); // That's it! No need to override: // - SupportsWeather() - automatically detected via HasWeatherSupport() @@ -87,9 +86,10 @@ void ExampleWeatherFeature::DrawSettings() ImGui::Separator(); ImGui::Spacing(); - ImGui::TextWrapped("Note: When weather-specific settings exist for the current weather, " - "they will automatically override these base settings. The weather system " - "handles all interpolation during weather transitions."); + ImGui::TextWrapped( + "Note: When weather-specific settings exist for the current weather, " + "they will automatically override these base settings. The weather system " + "handles all interpolation during weather transitions."); } void ExampleWeatherFeature::LoadSettings(json& o_json) diff --git a/src/Util.h b/src/Util.h index e8258f4de9..2363c06700 100644 --- a/src/Util.h +++ b/src/Util.h @@ -2,10 +2,10 @@ #include "Utils/D3D.h" #include "Utils/FileSystem.h" +#include "Utils/Form.h" #include "Utils/Format.h" #include "Utils/Game.h" #include "Utils/GameSetting.h" #include "Utils/Serialize.h" #include "Utils/UI.h" #include "Utils/WinApi.h" -#include "Utils/Form.h" diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 292ae4e255..069b754425 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1,7 +1,7 @@ #include "EditorWindow.h" -#include "State.h" #include "Features/WeatherEditor.h" +#include "State.h" #include "Weather/LightingTemplateWidget.h" #include "imgui_internal.h" @@ -87,68 +87,137 @@ void EditorWindow::ShowObjectsWindow() // Create a table for the right column with "Name" and "ID" headers. Different weights to prevent truncation. if (ImGui::BeginTable("DetailsTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f); // Largest - weather/template names - ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 80.0f); // Fixed - 8 hex chars - ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Medium - plugin names - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f); // Smaller - status text + ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 80.0f); // Fixed - 8 hex chars + ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Medium - plugin names + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f); // Smaller - status text ImGui::TableHeadersRow(); - // Display objects based on the selected category - auto& widgets = selectedCategory == "Weather" ? weatherWidgets : - selectedCategory == "WorldSpace" ? worldSpaceWidgets : - selectedCategory == "ImageSpace" ? imageSpaceWidgets : - lightingTemplateWidgets; - - // Get current cell's lighting template for prioritization - RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; - if (selectedCategory == "Lighting Template") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto& cellData = player->parentCell->GetRuntimeData(); - currentCellLightingTemplate = cellData.lightingTemplate; + // Display objects based on the selected category + auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + selectedCategory == "ImageSpace" ? imageSpaceWidgets : + lightingTemplateWidgets; + + // Get current cell's lighting template for prioritization + RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; + if (selectedCategory == "Lighting Template") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto& cellData = player->parentCell->GetRuntimeData(); + currentCellLightingTemplate = cellData.lightingTemplate; + } } - } - // Filtered display of widgets - show current cell's lighting template first - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + // Filtered display of widgets - show current cell's lighting template first + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + for (int i = 0; i < widgets.size(); ++i) { + auto* ltWidget = dynamic_cast(widgets[i]); + if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) + continue; + + if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) + continue; + + auto editorLabel = std::format("[CURRENT] {}", widgets[i]->GetEditorID()); + auto markedRecord = settings.markedRecords.find(widgets[i]->GetEditorID()); + ImGui::TableNextRow(); + + // Highlight current cell's lighting template + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + + ImGui::TableSetColumnIndex(0); + + // Editor ID column with [CURRENT] prefix + if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + widgets[i]->SetOpen(true); + } + } + + // Context menu + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; + + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[widgets[i]->GetEditorID()] = recordMarker.first; + Save(); + } + } + + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(widgets[i]->GetEditorID()); + Save(); + } + + ImGui::EndPopup(); + } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text(widgets[i]->GetFormID().c_str()); + + // File column + ImGui::TableNextColumn(); + ImGui::Text(widgets[i]->GetFilename().c_str()); + + // Status column + ImGui::TableNextColumn(); + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text("%s", markedRecord->second.c_str()); + } + } + } + + // Filtered display of widgets - regular list for (int i = 0; i < widgets.size(); ++i) { - auto* ltWidget = dynamic_cast(widgets[i]); - if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) - continue; + // Skip current cell's lighting template if already shown + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + auto* ltWidget = dynamic_cast(widgets[i]); + if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) + continue; + } if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) - continue; + continue; // Skip widgets that don't match the filter - auto editorLabel = std::format("[CURRENT] {}", widgets[i]->GetEditorID()); - auto markedRecord = settings.markedRecords.find(widgets[i]->GetEditorID()); + auto editorLabel = widgets[i]->GetEditorID(); + auto markedRecord = settings.markedRecords.find(editorLabel); ImGui::TableNextRow(); - // Highlight current cell's lighting template - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + // Set background colour + if (markedRecord != settings.markedRecords.end()) { + auto& color = settings.recordMarkers[markedRecord->second]; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); + } ImGui::TableSetColumnIndex(0); - // Editor ID column with [CURRENT] prefix + ImGui::TableSetColumnIndex(0); + + // Editor ID column if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { widgets[i]->SetOpen(true); } } - // Context menu + // Opens a context menu on right click to mark records by color if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; for (auto& recordMarker : settings.recordMarkers) { if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[widgets[i]->GetEditorID()] = recordMarker.first; + settings.markedRecords[editorLabel] = recordMarker.first; Save(); } } if (ImGui::MenuItem("Remove")) { - markedRecords.erase(widgets[i]->GetEditorID()); + markedRecords.erase(editorLabel); Save(); } @@ -165,82 +234,14 @@ void EditorWindow::ShowObjectsWindow() // Status column ImGui::TableNextColumn(); + + // Re-check if the record exists after potential removal + markedRecord = settings.markedRecords.find(editorLabel); if (markedRecord != settings.markedRecords.end()) { ImGui::Text("%s", markedRecord->second.c_str()); } } - } - - // Filtered display of widgets - regular list - for (int i = 0; i < widgets.size(); ++i) { - // Skip current cell's lighting template if already shown - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - auto* ltWidget = dynamic_cast(widgets[i]); - if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) - continue; - } - - if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) - continue; // Skip widgets that don't match the filter - - auto editorLabel = widgets[i]->GetEditorID(); - auto markedRecord = settings.markedRecords.find(editorLabel); - ImGui::TableNextRow(); - - // Set background colour - if (markedRecord != settings.markedRecords.end()) { - auto& color = settings.recordMarkers[markedRecord->second]; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); - } - - ImGui::TableSetColumnIndex(0); - - ImGui::TableSetColumnIndex(0); - - // Editor ID column - if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - widgets[i]->SetOpen(true); - } - } - - // Opens a context menu on right click to mark records by color - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { - auto& markedRecords = settings.markedRecords; - - for (auto& recordMarker : settings.recordMarkers) { - if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[editorLabel] = recordMarker.first; - Save(); - } - } - - if (ImGui::MenuItem("Remove")) { - markedRecords.erase(editorLabel); - Save(); - } - - ImGui::EndPopup(); - } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text(widgets[i]->GetFormID().c_str()); - - // File column - ImGui::TableNextColumn(); - ImGui::Text(widgets[i]->GetFilename().c_str()); - - // Status column - ImGui::TableNextColumn(); - - // Re-check if the record exists after potential removal - markedRecord = settings.markedRecords.find(editorLabel); - if (markedRecord != settings.markedRecords.end()) { - ImGui::Text("%s", markedRecord->second.c_str()); - } - } ImGui::EndTable(); + ImGui::EndTable(); } ImGui::EndTable(); @@ -330,11 +331,11 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Save All Open Widgets", "Ctrl+S")) { SaveAll(); } - + // Save individual widgets submenu if (ImGui::BeginMenu("Save")) { bool hasOpenWidgets = false; - + // Weather widgets for (auto* widget : weatherWidgets) { if (widget->IsOpen()) { @@ -344,7 +345,7 @@ void EditorWindow::RenderUI() } } } - + // WorldSpace widgets for (auto* widget : worldSpaceWidgets) { if (widget->IsOpen()) { @@ -354,7 +355,7 @@ void EditorWindow::RenderUI() } } } - + // Lighting Template widgets for (auto* widget : lightingTemplateWidgets) { if (widget->IsOpen()) { @@ -364,7 +365,7 @@ void EditorWindow::RenderUI() } } } - + // ImageSpace widgets for (auto* widget : imageSpaceWidgets) { if (widget->IsOpen()) { @@ -374,14 +375,14 @@ void EditorWindow::RenderUI() } } } - + if (!hasOpenWidgets) { ImGui::TextDisabled("No open widgets"); } - + ImGui::EndMenu(); } - + ImGui::Separator(); if (ImGui::MenuItem("Close All Weather Widgets")) { for (auto* widget : weatherWidgets) widget->SetOpen(false); @@ -413,7 +414,7 @@ void EditorWindow::RenderUI() if (ImGui::BeginMenu("Window")) { ImGui::Text("Open Widgets:"); ImGui::Separator(); - + int openCount = 0; for (auto* widget : weatherWidgets) { if (widget->IsOpen()) { @@ -447,11 +448,11 @@ void EditorWindow::RenderUI() } } } - + if (openCount == 0) { ImGui::TextDisabled("No widgets open"); } - + ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { @@ -470,7 +471,7 @@ void EditorWindow::RenderUI() ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); ImGui::EndMenu(); } - + // Weather lock indicator if (weatherLockActive && lockedWeather) { ImGui::SameLine(); @@ -479,7 +480,7 @@ void EditorWindow::RenderUI() ImGui::Text(" [LOCKED: %s]", weatherName ? weatherName : "Unknown"); ImGui::PopStyleColor(); } - + // Time pause indicator if (timePaused) { ImGui::SameLine(); @@ -487,7 +488,7 @@ void EditorWindow::RenderUI() ImGui::Text(" [TIME PAUSED]"); ImGui::PopStyleColor(); } - + // Close button on the right side float menuBarHeight = ImGui::GetFrameHeight(); ImGui::SameLine(ImGui::GetWindowWidth() - menuBarHeight - 10.0f); @@ -501,7 +502,7 @@ void EditorWindow::RenderUI() if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Close Weather Editor (Esc)"); } - + ImGui::EndMainMenuBar(); } @@ -574,7 +575,7 @@ void EditorWindow::Draw() { // Track editor open state for vanity camera management static bool wasOpen = false; - + if (open && !wasOpen) { // Editor just opened - disable vanity camera DisableVanityCamera(); @@ -582,7 +583,7 @@ void EditorWindow::Draw() // Editor just closed - restore vanity camera RestoreVanityCamera(); } - + wasOpen = open; // Re-enforce weather lock if active (handles time changes) @@ -689,11 +690,11 @@ void EditorWindow::ShowSettingsWindow() ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 60.0f); auto& recordMarkers = settings.recordMarkers; - + // Store markers to delete (can't delete while iterating) static std::string markerToDelete; markerToDelete.clear(); - + // Store rename info (old name -> new name) static std::pair renameInfo; static bool needsRename = false; @@ -701,12 +702,12 @@ void EditorWindow::ShowSettingsWindow() for (auto& recordMarker : recordMarkers) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - + // Editable label static char labelBuffer[256]; strncpy_s(labelBuffer, recordMarker.first.c_str(), sizeof(labelBuffer) - 1); labelBuffer[sizeof(labelBuffer) - 1] = '\0'; - + ImGui::SetNextItemWidth(-1); if (ImGui::InputText(std::format("##Label{}", recordMarker.first).c_str(), labelBuffer, sizeof(labelBuffer))) { // Mark for rename @@ -718,7 +719,7 @@ void EditorWindow::ShowSettingsWindow() if (ImGui::ColorEdit4(std::format("Color##{}", recordMarker.first).c_str(), (float*)&recordMarker.second)) { Save(); } - + ImGui::TableSetColumnIndex(2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { @@ -726,7 +727,7 @@ void EditorWindow::ShowSettingsWindow() } ImGui::PopStyleColor(); } - + // Process rename if (needsRename && renameInfo.first != renameInfo.second && !renameInfo.second.empty()) { // Check if new name doesn't already exist @@ -734,23 +735,23 @@ void EditorWindow::ShowSettingsWindow() auto color = recordMarkers[renameInfo.first]; recordMarkers.erase(renameInfo.first); recordMarkers[renameInfo.second] = color; - + // Update any records that were using the old marker name for (auto& [recordId, markerName] : settings.markedRecords) { if (markerName == renameInfo.first) { markerName = renameInfo.second; } } - + Save(); } needsRename = false; } - + // Process deletion if (!markerToDelete.empty()) { recordMarkers.erase(markerToDelete); - + // Remove any records that were using this marker for (auto it = settings.markedRecords.begin(); it != settings.markedRecords.end();) { if (it->second == markerToDelete) { @@ -759,7 +760,7 @@ void EditorWindow::ShowSettingsWindow() ++it; } } - + Save(); } @@ -839,10 +840,12 @@ void EditorWindow::Load() void EditorWindow::LockWeather(RE::TESWeather* weather) { - if (!weather) return; + if (!weather) + return; auto sky = RE::Sky::GetSingleton(); - if (!sky) return; + if (!sky) + return; // Force the weather to be active sky->ForceWeather(weather, false); @@ -855,7 +858,8 @@ void EditorWindow::LockWeather(RE::TESWeather* weather) void EditorWindow::UnlockWeather() { - if (!weatherLockActive) return; + if (!weatherLockActive) + return; auto sky = RE::Sky::GetSingleton(); if (sky) { @@ -871,7 +875,8 @@ void EditorWindow::UnlockWeather() void EditorWindow::PauseTime() { - if (timePaused) return; + if (timePaused) + return; auto calendar = RE::Calendar::GetSingleton(); if (calendar && calendar->timeScale) { @@ -884,7 +889,8 @@ void EditorWindow::PauseTime() void EditorWindow::ResumeTime() { - if (!timePaused) return; + if (!timePaused) + return; auto calendar = RE::Calendar::GetSingleton(); if (calendar && calendar->timeScale) { @@ -896,7 +902,8 @@ void EditorWindow::ResumeTime() void EditorWindow::DisableVanityCamera() { - if (vanityCameraDisabled) return; + if (vanityCameraDisabled) + return; auto setting = RE::GetINISetting("fAutoVanityModeDelay:Camera"); if (setting) { @@ -909,7 +916,8 @@ void EditorWindow::DisableVanityCamera() void EditorWindow::RestoreVanityCamera() { - if (!vanityCameraDisabled) return; + if (!vanityCameraDisabled) + return; auto setting = RE::GetINISetting("fAutoVanityModeDelay:Camera"); if (setting) { @@ -943,7 +951,7 @@ void EditorWindow::RenderNotifications() // Render active notifications for (auto& notif : notifications) { float elapsed = currentTime - notif.startTime; - float fadeStart = notif.duration - 0.5f; // Start fading 0.5s before end + float fadeStart = notif.duration - 0.5f; // Start fading 0.5s before end float alpha = 1.0f; // Fade out in the last 0.5 seconds @@ -961,7 +969,6 @@ void EditorWindow::RenderNotifications() if (ImGui::Begin(std::format("##Notification{}", (void*)¬if).c_str(), nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { - ImVec4 colorWithAlpha = notif.color; colorWithAlpha.w *= alpha; ImGui::PushStyleColor(ImGuiCol_Text, colorWithAlpha); diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp index abded0654b..435469ea15 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -186,7 +186,7 @@ void ImageSpaceWidget::LoadSettings() try { if (!js.empty() && js.contains("Settings")) { auto settingsJson = js["Settings"]; - + // Validate that we have actual data bool hasValidData = false; for (const auto& [key, value] : settingsJson.items()) { diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index 13fa16e931..1d864c167a 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -116,33 +116,41 @@ void LightingTemplateWidget::DrawBasicSettings() if (ImGui::CollapsingHeader("Ambient & Directional", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawColorEdit("Ambient Color", settings.ambient)) changed = true; + if (DrawColorEdit("Ambient Color", settings.ambient)) + changed = true; ImGui::Spacing(); - if (DrawColorEdit("Directional Color", settings.directional)) changed = true; + if (DrawColorEdit("Directional Color", settings.directional)) + changed = true; ImGui::Spacing(); } if (ImGui::CollapsingHeader("Directional Settings", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; + if (DrawSliderFloat("Directional XY", settings.directionalXY)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; + if (DrawSliderFloat("Directional Z", settings.directionalZ)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; + if (DrawSliderFloat("Directional Fade", settings.directionalFade)) + changed = true; ImGui::Spacing(); } if (ImGui::CollapsingHeader("Light Fade", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; + if (DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; + if (DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) + changed = true; ImGui::Spacing(); } if (ImGui::CollapsingHeader("Other", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; + if (DrawSliderFloat("Clip Distance", settings.clipDist)) + changed = true; ImGui::Spacing(); } @@ -156,21 +164,27 @@ void LightingTemplateWidget::DrawFogSettings() bool changed = false; ImGui::Spacing(); - if (DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; + if (DrawColorEdit("Fog Color Near", settings.fogColorNear)) + changed = true; ImGui::Spacing(); - if (DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; + if (DrawColorEdit("Fog Color Far", settings.fogColorFar)) + changed = true; ImGui::Spacing(); ImGui::Spacing(); - if (DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; + if (DrawSliderFloat("Fog Near", settings.fogNear)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; + if (DrawSliderFloat("Fog Far", settings.fogFar)) + changed = true; ImGui::Spacing(); ImGui::Spacing(); - if (DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; + if (DrawSliderFloat("Fog Power", settings.fogPower)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; + if (DrawSliderFloat("Fog Clamp", settings.fogClamp)) + changed = true; ImGui::Spacing(); if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { @@ -183,17 +197,21 @@ void LightingTemplateWidget::DrawDALCSettings() bool changed = false; ImGui::Spacing(); - if (DrawColorEdit("Specular", settings.dalc.specular)) changed = true; + if (DrawColorEdit("Specular", settings.dalc.specular)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; + if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) + changed = true; ImGui::Spacing(); for (int j = 0; j < 3; j++) { const char* labels[] = { "X", "Y", "Z" }; ImGui::Separator(); ImGui::Text("Directional %s", labels[j]); - if (DrawColorEdit(std::format("Max##{}", labels[j]), settings.dalc.directional[j].max)) changed = true; - if (DrawColorEdit(std::format("Min##{}", labels[j]), settings.dalc.directional[j].min)) changed = true; + if (DrawColorEdit(std::format("Max##{}", labels[j]), settings.dalc.directional[j].max)) + changed = true; + if (DrawColorEdit(std::format("Min##{}", labels[j]), settings.dalc.directional[j].min)) + changed = true; ImGui::Spacing(); } diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 88e1236688..7f479346fc 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -46,13 +46,12 @@ void WeatherWidget::DrawWidget() { ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - auto editorWindow = EditorWindow::GetSingleton(); bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; // Weather lock and time controls (inline) float buttonWidth = ImGui::GetContentRegionAvail().x * 0.49f; - + // Weather lock controls if (isLocked) { if (ImGui::Button("Unlock Weather", ImVec2(buttonWidth, 0))) { @@ -154,7 +153,7 @@ void WeatherWidget::DrawWidget() bool hasApplyButton = !editorWindow->settings.autoApplyChanges; int buttonCount = hasApplyButton ? 4 : 3; float textButtonWidth = ImGui::GetContentRegionAvail().x / buttonCount; - + // Apply button - only show when auto-apply is disabled if (hasApplyButton) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); @@ -167,7 +166,7 @@ void WeatherWidget::DrawWidget() } ImGui::SameLine(); } - + // Save button - always visible if (ImGui::Button("Save", ImVec2(textButtonWidth * 0.97f, 0))) { Save(); @@ -175,9 +174,9 @@ void WeatherWidget::DrawWidget() if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Save to file"); } - + ImGui::SameLine(); - + // Load button - always visible if (ImGui::Button("Load", ImVec2(textButtonWidth * 0.97f, 0))) { Load(); @@ -185,9 +184,9 @@ void WeatherWidget::DrawWidget() if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Load from file"); } - + ImGui::SameLine(); - + // Delete button - always visible float deleteIconSize = ImGui::GetFrameHeight(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); @@ -204,7 +203,7 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Delete saved file and revert to defaults"); } } - + // Confirmation popup for delete if (ImGui::BeginPopupModal("ConfirmDelete", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Are you sure you want to delete this file?"); @@ -212,14 +211,14 @@ void WeatherWidget::DrawWidget() ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - + if (ImGui::Checkbox("Don't show this warning again", &editorWindow->settings.suppressDeleteWarning)) { // Save the preference immediately editorWindow->Save(); } - + ImGui::Spacing(); - + if (ImGui::Button("Yes", ImVec2(120, 0))) { Delete(); ImGui::CloseCurrentPopup(); @@ -231,7 +230,7 @@ void WeatherWidget::DrawWidget() } ImGui::EndPopup(); } - + ImGui::Separator(); auto& widgets = editorWindow->weatherWidgets; @@ -330,12 +329,12 @@ void WeatherWidget::DrawWidget() void WeatherWidget::LoadSettings() { bool hadErrors = false; - + if (!js.empty()) { try { // Attempt to load settings from JSON settings = js; - + // Validate that critical fields were loaded correctly if (js.contains("weatherProperties") && settings.weatherProperties.empty() && !js["weatherProperties"].empty()) { logger::warn("Weather {}: weatherProperties loaded but appears empty, reverting to vanilla values", GetEditorID()); @@ -349,7 +348,7 @@ void WeatherWidget::LoadSettings() logger::warn("Weather {}: fogProperties loaded but appears empty, reverting to vanilla values", GetEditorID()); hadErrors = true; } - + if (hadErrors) { // Fallback to vanilla/game values LoadWeatherValues(); @@ -364,7 +363,7 @@ void WeatherWidget::LoadSettings() settings.weatherColors.size(), settings.fogProperties.size()); } - + } catch (const nlohmann::json::exception& e) { logger::error("Weather {}: Failed to deserialize settings from JSON: {}", GetEditorID(), e.what()); logger::error("JSON content: {}", js.dump(2)); @@ -387,17 +386,17 @@ void WeatherWidget::LoadSettings() void WeatherWidget::SaveSettings() { SaveFeatureSettings(); - + try { js = settings; - + // Log what we're saving for debugging logger::info("Weather {}: Saving settings - {} weather properties, {} colors, {} fog properties", GetEditorID(), settings.weatherProperties.size(), settings.weatherColors.size(), settings.fogProperties.size()); - + // Validate serialization worked if (js.is_null()) { logger::error("Weather {}: Serialization produced null JSON!", GetEditorID()); @@ -408,7 +407,7 @@ void WeatherWidget::SaveSettings() } else if (!js.contains("clouds")) { logger::error("Weather {}: Serialized JSON missing clouds field!", GetEditorID()); } - + } catch (const nlohmann::json::exception& e) { logger::error("Weather {}: Failed to serialize settings to JSON: {}", GetEditorID(), e.what()); } @@ -630,14 +629,18 @@ void WeatherWidget::DrawDALCSettings() if (ImGui::CollapsingHeader(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { ImGui::Spacing(); - if (DrawColorEdit(std::format("Specular##{}", label), settings.dalc[i].specular)) changed = true; + if (DrawColorEdit(std::format("Specular##{}", label), settings.dalc[i].specular)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat(std::format("Fresnel Power##{}", label), settings.dalc[i].fresnelPower)) changed = true; + if (DrawSliderFloat(std::format("Fresnel Power##{}", label), settings.dalc[i].fresnelPower)) + changed = true; ImGui::Spacing(); for (int j = 0; j < 3; j++) { - if (DrawColorEdit(std::format("DALC X Max##{}", label), settings.dalc[i].directional[j].max)) changed = true; - if (DrawColorEdit(std::format("DALC X Min##{}", label), settings.dalc[i].directional[j].min)) changed = true; + if (DrawColorEdit(std::format("DALC X Max##{}", label), settings.dalc[i].directional[j].max)) + changed = true; + if (DrawColorEdit(std::format("DALC X Min##{}", label), settings.dalc[i].directional[j].min)) + changed = true; ImGui::Spacing(); } } @@ -666,7 +669,8 @@ void WeatherWidget::DrawWeatherColorSettings() if (ImGui::CollapsingHeader(colorTypeLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { for (int j = 0; j < ColorTimes::kTotal; j++) { - if (DrawColorEdit(std::format("{}##{}", ColorTimeLabel(j), colorTypeLabel), settings.atmosphereColors[i].colorTimes[j])) changed = true; + if (DrawColorEdit(std::format("{}##{}", ColorTimeLabel(j), colorTypeLabel), settings.atmosphereColors[i].colorTimes[j])) + changed = true; ImGui::Spacing(); } } @@ -694,17 +698,21 @@ void WeatherWidget::DrawCloudSettings() std::string layer = std::format("Layer {}", i); if (ImGui::CollapsingHeader(layer.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; + if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) + changed = true; ImGui::Spacing(); - if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; + if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) + changed = true; for (int j = 0; j < ColorTimes::kTotal; j++) { std::string colorTime = ColorTimeLabel(j).c_str(); if (ImGui::CollapsingHeader(colorTime.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - if (DrawColorEdit(std::format("Cloud Color##{}{}", colorTime, i), settings.clouds[i].color[j])) changed = true; + if (DrawColorEdit(std::format("Cloud Color##{}{}", colorTime, i), settings.clouds[i].color[j])) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat(std::format("Cloud Alpha##{}{}", colorTime, i), settings.clouds[i].cloudAlpha[j])) changed = true; + if (DrawSliderFloat(std::format("Cloud Alpha##{}{}", colorTime, i), settings.clouds[i].cloudAlpha[j])) + changed = true; ImGui::Spacing(); } } @@ -732,16 +740,20 @@ void WeatherWidget::DrawProperties(std::string category, std::mapSaveSettingsToWeather(weather, featureName, featureJson); @@ -782,14 +794,14 @@ void WeatherWidget::LoadFeatureSettings() { auto* weatherManager = WeatherManager::GetSingleton(); auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); - + for (auto* feature : Feature::GetFeatureList()) { if (!feature || !feature->loaded) { continue; } std::string featureName = feature->GetShortName(); - + // Check if feature has registered weather variables if (!globalRegistry->HasWeatherSupport(featureName)) { continue; @@ -827,7 +839,7 @@ void WeatherWidget::DrawFeatureSettings() } std::string featureName = feature->GetShortName(); - + // Check if feature has registered weather variables if (!globalRegistry->HasWeatherSupport(featureName)) { continue; @@ -845,7 +857,7 @@ void WeatherWidget::DrawFeatureSettings() if (hasSettings) { ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); - + if (ImGui::Button("Clear Settings")) { settings.featureSettings[featureName] = json::object(); } @@ -866,8 +878,9 @@ void WeatherWidget::DrawFeatureSettings() } ImGui::Spacing(); - ImGui::TextWrapped("Note: Feature settings should be configured through the feature's own settings panel. " - "This section shows which features have per-weather overrides."); + ImGui::TextWrapped( + "Note: Feature settings should be configured through the feature's own settings panel. " + "This section shows which features have per-weather overrides."); ImGui::TreePop(); } diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index e8ef3309fa..84dae7ba47 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -39,7 +39,7 @@ void Widget::Save() } logger::info("{}: Saving settings file: {}", GetEditorID(), file); - + // Write with indentation for readability settingsFile << js.dump(2); settingsFile.flush(); @@ -52,7 +52,7 @@ void Widget::Save() settingsFile.close(); logger::info("{}: Successfully saved settings", GetEditorID()); - + } catch (const nlohmann::json::exception& e) { logger::error("{}: JSON error while saving settings: {}", GetEditorID(), e.what()); settingsFile.close(); @@ -84,7 +84,7 @@ void Widget::Load() try { settingsFile >> js; settingsFile.close(); - + // Validate that we loaded valid JSON if (js.is_null()) { logger::warn("{}: Loaded JSON is null, file may be empty or invalid", filePath); @@ -94,9 +94,9 @@ void Widget::Load() 3.0f); return; } - + logger::info("{}: Successfully loaded settings file", GetEditorID()); - + } catch (const nlohmann::json::parse_error& e) { logger::error("Error parsing settings for file ({}) : {}\n", filePath, e.what()); logger::error("Parse error at byte {}: {}", e.byte, e.what()); @@ -130,13 +130,13 @@ void Widget::Delete() try { std::filesystem::remove(filePath); logger::info("Deleted settings file: {}", filePath); - + // Clear the in-memory JSON data js = json(); - + // Reload settings from vanilla/mod defaults LoadSettings(); - + EditorWindow::GetSingleton()->ShowNotification( std::format("Deleted {} - reverted to vanilla values", GetEditorID()), ImVec4(0.0f, 1.0f, 0.0f, 1.0f), @@ -166,7 +166,7 @@ void Widget::DrawMenu() ImGui::OpenPopup("ConfirmDelete"); } } - + // Confirmation popup for delete if (ImGui::BeginPopupModal("ConfirmDelete", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Are you sure you want to delete this file?"); @@ -174,15 +174,15 @@ void Widget::DrawMenu() ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - + auto& settings = EditorWindow::GetSingleton()->settings; if (ImGui::Checkbox("Don't show this warning again", &settings.suppressDeleteWarning)) { // Save the preference immediately EditorWindow::GetSingleton()->Save(); } - + ImGui::Spacing(); - + if (ImGui::Button("Yes", ImVec2(120, 0))) { Delete(); ImGui::CloseCurrentPopup(); @@ -194,7 +194,7 @@ void Widget::DrawMenu() } ImGui::EndPopup(); } - + ImGui::EndMenu(); } ImGui::EndMenuBar(); From 4b35846ffa0d7ab9b96004b4f2175acf26c8be10 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 8 Dec 2025 23:33:03 +1000 Subject: [PATCH 05/30] heaps of fixes --- .../Icons/Action Icons/pause.png | Bin 0 -> 3230 bytes src/Menu.cpp | 2 + src/Menu.h | 1 + src/Utils/UI.cpp | 1 + src/WeatherEditor/EditorWindow.cpp | 559 ++++++++++-- src/WeatherEditor/EditorWindow.h | 16 + .../Weather/ImageSpaceWidget.cpp | 248 ++++-- src/WeatherEditor/Weather/ImageSpaceWidget.h | 6 - .../Weather/LightingTemplateWidget.cpp | 144 ++- src/WeatherEditor/Weather/WeatherWidget.cpp | 838 ++++++++++++++---- src/WeatherEditor/Weather/WeatherWidget.h | 19 + .../Weather/WorldSpaceWidget.cpp | 21 +- src/WeatherEditor/WeatherUtils.cpp | 316 +++++++ src/WeatherEditor/WeatherUtils.h | 59 +- src/WeatherEditor/Widget.cpp | 62 +- src/WeatherEditor/Widget.h | 15 +- 16 files changed, 1939 insertions(+), 368 deletions(-) create mode 100644 package/Interface/CommunityShaders/Icons/Action Icons/pause.png diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/pause.png b/package/Interface/CommunityShaders/Icons/Action Icons/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..4763dab681fceea50bf5f1efc8f2f1fb9f06ab9d GIT binary patch literal 3230 zcmV;P3}N$$P)@~0drDELIAGL9O(c600d`2O+f$vv5yP5Wh(hviJ=tiUQO|-GGv9WQYoY9E^;Jp1f?|uDpc6RncC&;Q4Knu5Mugmtj z(0_N=kl_2g=&RA6&+YY{|8EuKnN~>F3ZMmk)n4!0t0X1(n6MH&w%0fQyH$#YGABC) zXoX&q67~oExg?!J+~;Z`vQPkB(!aFVCsO7-MVj`yZ?C&GScW9v(%xPb@}p|6Pm}@4 zSpcZg&Y8z0zM$}Qj@}^!#i41hU275UDN&MAfL8ds_Tw|zy>$~T+(%NG|4gsm{QqrD zJ-oIe{zb~6EcEB)#56UOxFReHl&P;Vj=H^W+2pp@1x2EbyJ)YE?X}0Wv}RE?%!1C; z3jS2>brVzIj%okgUQh7nfqze@Oq&5a`0+yv{XHdyR)#(NEyIpZ4V_s&rKW;1?^EjF zp>$kprK%CoEbDk{_t*OwwxCm>ebFyE(_IDsfg*upA^o7haN6Og2w!Nev@dtwUU!Ze zQO6YgL7I-BNWjk`urnLf(k%|$P5#e1o@Qb$1vq$~tM#j~mMWorkxRH~e>L|@&#eIZ znVqw^ekFiv+uJmr?|-HRSV92~p6y}{_fV&*&IC+ICe~HS`erXE*n>rGEfemsb2e$k|yJ|8#;+@ zS=cQn=U?m@Pvd~SK1(<83E@zP>J*gq2&hh;dW`U)yy04x!gY0k7{IIb;c95tyXT?= z>Ex9b-WcJRy|7P7y;wx>wqva9-M1;h1Em1$BYIbc-T+$N2Uhg|K^$hVu*KRG0ANsnb$OyvZcR#oM z&eTs}{e2WLY{ojCJmZpD5G6%INPo2c2@G4y8jc=4%(JGVD{ntBCevSIL^B1$PESmM z3^`cA=L8%)@23AOcr=T3o|~M)lI8+t(?7bS2XCF8f+8U$o+5#_4zG7U!Q9L0Wpzgb zvu8TlgNL#NH;6nTM0x>=NQifX*&m4OH^JQ7$KMx}2}>wI2%wy1sVd;_<2i&dxB0Ug zvdmQh7?`ba9KQ*c(odO@vjQ-XTLp2f0L-O7xq13FR-gc-K_20)0OZo2W!G))3cz44 zD*$tyQ&5JiL;*}zAly7l$fducOj&~hFp#Z47};2~Ec)V&Sgc3^O!6sdDv;-}XXp7J zvK9qkATPR^K1Kl9{Aaz!ePs%8BfQSEu??8bOFmb{+;2TRLUQ>U9i$Y zZ+r77-2O-wObSp6-`~+jYWyjDKW;4n%wzrqWzL$a0LD@cT$p|36>jd?#s3bNCO`tkxSH{#oDW!xf%4MTrAmx3c{}#|L5jrk(R=8W#3@ z5`2VTgup}eBX~G?SV_kTWd-22>F-;sVH&#G&Heh_-o-<(EdE1*EPxca>HTl=2l(E# zj_X(2m;z6_DJ@~TYWurzzYxA3^Z}&6eGBkA**Tdd-w|=i!=3(=s@_8Q-pdC#qBZTj zyo${{R~g-w>IEG;kuFNIVdf7~R-mIH)g15qF7~1Ew;}Qus8FOm} z>L#8w{~>qbyU*W%yzpQ(<~%nAa0PN-fH*hcY#@@sjA+fwYR(v3^E>*!n`t8wT-lK8pk=rrwaHt1wuDh3VkJGS&;vcZc<)~ zgL6YYfej7!9C1g4H0xv#s1ZXRxjH<2u^0%~J-55~Io}pIkogX3o4WsIdKh#t1@MXd z2&lQ}T#~NHZ!Dmoo^rHtHKe*d|JMrSPYMTT$hjdMyxQiD@*B;RMsgeJ2k$%rUkcxM zCVi)SoOog4HH8@nDs)`D9c`_jQP6bm9Ku|aQiL7lLm_GVl)|@7ru$AaWSVrIZQ1a6V@M#-PSTp}_svmX19VUKas*p{V;xZdfK4 zV*Gi(hjb@))|gI(i9&(@X`eev41}D_Ysicn_?3DNTj}3@gkeY%%JO{FVJrAiW8#r2 zP>P*xwE%fBum2{Ja68-1M1_gx{X%K>L930~eVc1pxll0t=v^g@9a8{H+x1X94u7Fwc3vu1?#I0s*%Zy*g?vP%XeLugkHK zy4LlxpZw`4lpO^Eein$SF{3qr+;ktml@II=QH=#p0)yMR#FHGufoJ+u3$yK_2H9uX zkBP6-_~ef-gWGX+1qB94nxeJ?#yO%!wQxYqv`gl}J0IybskfSG_dvVJLJ6xsfN#&cr-t(gXl=@1-9Q$kz8wCzC~9qdI@~h6oGt z1fJ*e`Gz;dW2y?0wgRyGUsnfV_l9r<`GSDX1U%2>L*R4#44IRI6|nVhbR)6|B`9NcK`TyL*QM(rPPHA@0bQDqoSP<8Vl&<;fQf`AI#=*RDCH$7Am}B8cb*GEjETR z1(?rl9ycfldF+357pj!PP~kn&Q6XezEeOnh+URa)o)%sR_Z}&Kq{STtWdvS#3X`~| zk+1?t<}k;VF@3yu%}Nw@?h4`F-PzgMHyQ~mfK<=_Mfo#o1Lc%}n%Ghbw;P(e5f>ut z2_#iip~8ElL+UWeq9p)x&mc2>8eWXd_l?sxe9W|@0?e&5CQP?>j12OP(>Hv~w4{I* zzG=o$ut_f47BPtd)knbsHf{Fs8wGrzq-hs465mqi8{Q)w-uvb)i9^W`KF5(QfV6o) zupL1bq!7MuXaaa$wG^y10X0ZvD*4Q>q4z6b1(2Fb?(A+26N+k~iK8|0|h$`4|@dJ;ea>sM(sw0!EE; z>Ntn`7QFjR;J(a$bXr{#vgXl1iAmNCZw2RN1)v6YYzs^Heq7-nBuGlk8s`Iy9yvRR zyFY^J)T)3|N|&YQERXrBU~DsjeB&OPYfxH#LWOsDeqneY z_YITG&*+!rTOl0hZ3QRA63n zUj~TlHdJ`avrVOc^m+r~S=|!)3fgSLU+^_jn4$*r96E1bKxa<3(8KB_`-V5fQyVh7 zpa@|6eu@0f>8w+L*$PBkVH*l91c{*2!L`u2HBiBy3Nvh};|OgI*XOEzzs4#J(%*BDZbBL+2piiLFx8!Ek zeSR)$M~cqfSYV%(%Is)nU|u3MCpFe4^geLgl#w%{q{u*(`X?+$4doPa^KbN&xs-4^ zDix|@X6F#}uD$MChv$(30;OD>hSDMUSN+*d+$hM8hLrQx(D4~Y-?MV$LIb}&bGej$ z!t_%;d^=h`8{)dKT=_a|6Yx4LS(il9I<+AlNuL5nxuiIcons.logo.texture, menu->uiIcons.logo.size, "logo"); loadIconWithLogging(basePath + "Action Icons\\restore-settings.png", &menu->uiIcons.featureSettingRevert.texture, menu->uiIcons.featureSettingRevert.size, "restore-settings"); loadIconWithLogging(basePath + "Action Icons\\apply-to-game.png", &menu->uiIcons.applyToGame.texture, menu->uiIcons.applyToGame.size, "apply-to-game"); + loadIconWithLogging(basePath + "Action Icons\\pause.png", &menu->uiIcons.pauseTime.texture, menu->uiIcons.pauseTime.size, "pause"); loadIconWithLogging(basePath + "Action Icons\\discord.png", &menu->uiIcons.discord.texture, menu->uiIcons.discord.size, "discord"); // Load category icons in a more compact way diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 292ae4e255..5297c19086 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -3,17 +3,10 @@ #include "State.h" #include "Features/WeatherEditor.h" #include "Weather/LightingTemplateWidget.h" +#include "WeatherUtils.h" #include "imgui_internal.h" -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges) - -bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring) -{ - const auto it = std::ranges::search(a_string, a_substring, [](const char a_a, const char a_b) { - return std::tolower(a_a) == std::tolower(a_b); - }).begin(); - return it != a_string.end(); -} +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) void TextUnformattedDisabled(const char* a_text, const char* a_textEnd = nullptr) { @@ -43,15 +36,119 @@ inline void HelpMarker(const char* a_desc) AddTooltip(a_desc, ImGuiHoveredFlags_DelayShort); } +void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool filled) +{ + auto* drawList = ImGui::GetWindowDrawList(); + const int numPoints = 5; + const float angleStep = 3.14159f / numPoints; + ImVec2 points[10]; + + for (int i = 0; i < numPoints * 2; i++) { + float angle = -1.57079f + i * angleStep; + float r = (i % 2 == 0) ? radius : radius * 0.38f; + points[i] = ImVec2(center.x + cosf(angle) * r, center.y + sinf(angle) * r); + } + + if (filled) { + // Draw filled star using triangles to avoid artifacts + for (int i = 0; i < numPoints; i++) { + int outerIdx1 = i * 2; + int innerIdx = (i * 2 + 1) % 10; + int outerIdx2 = (i * 2 + 2) % 10; + drawList->AddTriangleFilled(center, points[outerIdx1], points[innerIdx], color); + drawList->AddTriangleFilled(center, points[innerIdx], points[outerIdx2], color); + } + } else { + for (int i = 0; i < 10; i++) { + drawList->AddLine(points[i], points[(i + 1) % 10], color, 1.5f); + } + } +} + +void DrawIconCircle(ImVec2 center, float radius, ImU32 color, bool filled) +{ + auto* drawList = ImGui::GetWindowDrawList(); + if (filled) { + drawList->AddCircleFilled(center, radius, color, 16); + } else { + drawList->AddCircle(center, radius, color, 16, 1.5f); + } +} + +void DrawIconWave(ImVec2 center, float width, ImU32 color, bool filled) +{ + auto* drawList = ImGui::GetWindowDrawList(); + const int segments = 8; + const float amplitude = width * 0.15f; + const float waveWidth = width * 0.8f; + const float segmentWidth = waveWidth / segments; + + ImVec2 start(center.x - waveWidth * 0.5f, center.y); + + if (filled) { + // Draw filled wave using multiple horizontal lines + for (int i = 0; i < segments; i++) { + float x1 = start.x + i * segmentWidth; + float x2 = start.x + (i + 1) * segmentWidth; + float y1 = start.y + sinf(i * 3.14159f / 2.0f) * amplitude; + float y2 = start.y + sinf((i + 1) * 3.14159f / 2.0f) * amplitude; + drawList->AddLine(ImVec2(x1, y1), ImVec2(x2, y2), color, 3.0f); + } + } else { + // Draw outline wave + for (int i = 0; i < segments; i++) { + float x1 = start.x + i * segmentWidth; + float x2 = start.x + (i + 1) * segmentWidth; + float y1 = start.y + sinf(i * 3.14159f / 2.0f) * amplitude; + float y2 = start.y + sinf((i + 1) * 3.14159f / 2.0f) * amplitude; + drawList->AddLine(ImVec2(x1, y1), ImVec2(x2, y2), color, 1.5f); + } + } +} + +bool IconButton(const char* label, bool filled, const char* iconType) +{ + ImVec2 buttonSize(ImGui::GetFrameHeight(), ImGui::GetFrameHeight()); + ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + + bool result = ImGui::InvisibleButton(label, buttonSize); + + bool hovered = ImGui::IsItemHovered(); + bool active = ImGui::IsItemActive(); + + ImU32 bgColor = active ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : + hovered ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) : + ImGui::GetColorU32(ImGuiCol_Button); + ImU32 iconColor = ImGui::GetColorU32(ImGuiCol_Text); + + auto* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(cursorPos, ImVec2(cursorPos.x + buttonSize.x, cursorPos.y + buttonSize.y), bgColor, ImGui::GetStyle().FrameRounding); + + ImVec2 center(cursorPos.x + buttonSize.x * 0.5f, cursorPos.y + buttonSize.y * 0.5f); + float iconSize = buttonSize.x * 0.35f; + + if (strcmp(iconType, "star") == 0) { + DrawIconStar(center, iconSize, iconColor, filled); + } else if (strcmp(iconType, "circle") == 0) { + DrawIconCircle(center, iconSize, iconColor, filled); + } else if (strcmp(iconType, "wave") == 0) { + DrawIconWave(center, buttonSize.x * 0.7f, iconColor, filled); + } + + return result; +} + void EditorWindow::ShowObjectsWindow() { - ImGui::Begin("Object List"); + ImGui::Begin("Weather and Lighting Browser"); // Static variable to track the selected category static std::string selectedCategory = "Lighting Template"; // Static variable for filtering objects static char filterBuffer[256] = ""; + static bool showOnlyFlagged = false; + static bool showOnlyFavorites = false; // Create a table with two columns if (ImGui::BeginTable("ObjectTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner | ImGuiTableFlags_NoHostExtendX)) { @@ -68,7 +165,7 @@ void EditorWindow::ShowObjectsWindow() ImGui::Spacing(); // List of categories - const char* categories[] = { "ImageSpace", "Lighting Template", "Weather", "WorldSpace" }; + const char* categories[] = { "Lighting Template", "Weather", "WorldSpace" }; for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { // Highlight the selected category if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { @@ -79,26 +176,134 @@ void EditorWindow::ShowObjectsWindow() // Right column: Objects ImGui::TableSetColumnIndex(1); - ImGui::InputTextWithHint("##ObjectFilter", "Filter...", filterBuffer, sizeof(filterBuffer)); + // Handle Ctrl+F to focus search bar + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(); + } + } + + ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); + + ImGui::SameLine(); + HelpMarker("Type a part of an object name to filter the list.\nCtrl+F: Focus search\nEnter: Open selected"); + // Quick filter buttons on same row ImGui::SameLine(); - HelpMarker("Type a part of an object name to filter the list."); + ImGui::Dummy(ImVec2(10.0f, 0.0f)); // Spacer + ImGui::SameLine(); + if (IconButton("##filterFavorites", showOnlyFavorites, "star")) { + showOnlyFavorites = !showOnlyFavorites; + } + ImGui::SameLine(); + ImGui::Text("Favorites"); + + ImGui::SameLine(); + ImGui::Dummy(ImVec2(10.0f, 0.0f)); // Spacer + ImGui::SameLine(); + if (IconButton("##filterFlagged", showOnlyFlagged, "circle")) { + showOnlyFlagged = !showOnlyFlagged; + } + ImGui::SameLine(); + ImGui::Text("Flagged"); + + // Show recent widgets section if there are any + if (!settings.recentWidgets.empty()) { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Recent:"); + ImGui::SameLine(); + for (size_t i = 0; i < std::min(size_t(5), settings.recentWidgets.size()); ++i) { + if (i > 0) ImGui::SameLine(); + if (ImGui::SmallButton(settings.recentWidgets[i].c_str())) { + // Find and open widget in all collections + bool found = false; + for (auto* widget : weatherWidgets) { + if (widget->GetEditorID() == settings.recentWidgets[i]) { + widget->SetOpen(true); + found = true; + break; + } + } + if (!found) { + for (auto* widget : worldSpaceWidgets) { + if (widget->GetEditorID() == settings.recentWidgets[i]) { + widget->SetOpen(true); + found = true; + break; + } + } + } + if (!found) { + for (auto* widget : lightingTemplateWidgets) { + if (widget->GetEditorID() == settings.recentWidgets[i]) { + widget->SetOpen(true); + break; + } + } + } + } + } + } // Create a table for the right column with "Name" and "ID" headers. Different weights to prevent truncation. - if (ImGui::BeginTable("DetailsTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) { + if (ImGui::BeginTable("DetailsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Sortable)) { + ImGui::TableSetupColumn("Fav", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoSort, 25.0f); // Favorite indicator ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f); // Largest - weather/template names ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 80.0f); // Fixed - 8 hex chars ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Medium - plugin names ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f); // Smaller - status text ImGui::TableHeadersRow(); + + // Handle column sorting + if (ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs()) { + if (sortSpecs->SpecsDirty) { + if (sortSpecs->SpecsCount > 0) { + const ImGuiTableColumnSortSpecs& spec = sortSpecs->Specs[0]; + currentSortColumn = static_cast(spec.ColumnIndex); + sortAscending = (spec.SortDirection == ImGuiSortDirection_Ascending); + } else { + currentSortColumn = SortColumn::None; + } + sortSpecs->SpecsDirty = false; + } + } // Display objects based on the selected category auto& widgets = selectedCategory == "Weather" ? weatherWidgets : selectedCategory == "WorldSpace" ? worldSpaceWidgets : - selectedCategory == "ImageSpace" ? imageSpaceWidgets : lightingTemplateWidgets; + // Sort widgets based on current sort column + std::vector sortedWidgets = widgets; + if (currentSortColumn != SortColumn::None) { + std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { + int comparison = 0; + switch (currentSortColumn) { + case SortColumn::EditorID: + comparison = _stricmp(a->GetEditorID().c_str(), b->GetEditorID().c_str()); + break; + case SortColumn::FormID: + comparison = _stricmp(a->GetFormID().c_str(), b->GetFormID().c_str()); + break; + case SortColumn::File: + comparison = _stricmp(a->GetFilename().c_str(), b->GetFilename().c_str()); + break; + case SortColumn::Status: { + auto markerA = settings.markedRecords.find(a->GetEditorID()); + auto markerB = settings.markedRecords.find(b->GetEditorID()); + std::string statusA = (markerA != settings.markedRecords.end()) ? markerA->second : ""; + std::string statusB = (markerB != settings.markedRecords.end()) ? markerB->second : ""; + comparison = _stricmp(statusA.c_str(), statusB.c_str()); + break; + } + default: + break; + } + return sortAscending ? (comparison < 0) : (comparison > 0); + }); + } + // Get current cell's lighting template for prioritization RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; if (selectedCategory == "Lighting Template") { @@ -111,16 +316,22 @@ void EditorWindow::ShowObjectsWindow() // Filtered display of widgets - show current cell's lighting template first if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - for (int i = 0; i < widgets.size(); ++i) { - auto* ltWidget = dynamic_cast(widgets[i]); + for (int i = 0; i < sortedWidgets.size(); ++i) { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) continue; - if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) continue; - auto editorLabel = std::format("[CURRENT] {}", widgets[i]->GetEditorID()); - auto markedRecord = settings.markedRecords.find(widgets[i]->GetEditorID()); + // Apply quick filters + if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) + continue; + if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) + continue; + + auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); + auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); ImGui::TableNextRow(); // Highlight current cell's lighting template @@ -129,26 +340,40 @@ void EditorWindow::ShowObjectsWindow() ImGui::TableSetColumnIndex(0); + // Favorite star + if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + ToggleFavorite(sortedWidgets[i]->GetEditorID()); + } + + ImGui::TableNextColumn(); + // Editor ID column with [CURRENT] prefix - if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + bool isSelected = sortedWidgets[i]->IsOpen(); + if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { - widgets[i]->SetOpen(true); + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); } } + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); + } // Context menu - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; for (auto& recordMarker : settings.recordMarkers) { if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[widgets[i]->GetEditorID()] = recordMarker.first; + settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; Save(); } } if (ImGui::MenuItem("Remove")) { - markedRecords.erase(widgets[i]->GetEditorID()); + markedRecords.erase(sortedWidgets[i]->GetEditorID()); Save(); } @@ -157,11 +382,11 @@ void EditorWindow::ShowObjectsWindow() // Form ID column ImGui::TableNextColumn(); - ImGui::Text(widgets[i]->GetFormID().c_str()); + ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); // File column ImGui::TableNextColumn(); - ImGui::Text(widgets[i]->GetFilename().c_str()); + ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); // Status column ImGui::TableNextColumn(); @@ -172,18 +397,24 @@ void EditorWindow::ShowObjectsWindow() } // Filtered display of widgets - regular list - for (int i = 0; i < widgets.size(); ++i) { + for (int i = 0; i < sortedWidgets.size(); ++i) { // Skip current cell's lighting template if already shown if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - auto* ltWidget = dynamic_cast(widgets[i]); + auto* ltWidget = dynamic_cast(sortedWidgets[i]); if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) continue; } - if (!ContainsStringIgnoreCase(widgets[i]->GetEditorID(), filterBuffer)) - continue; // Skip widgets that don't match the filter + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + continue; + + // Apply quick filters + if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) + continue; + if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) + continue; - auto editorLabel = widgets[i]->GetEditorID(); + auto editorLabel = sortedWidgets[i]->GetEditorID(); auto markedRecord = settings.markedRecords.find(editorLabel); ImGui::TableNextRow(); @@ -196,17 +427,29 @@ void EditorWindow::ShowObjectsWindow() ImGui::TableSetColumnIndex(0); - ImGui::TableSetColumnIndex(0); + // Favorite star + if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + ToggleFavorite(sortedWidgets[i]->GetEditorID()); + } + + ImGui::TableNextColumn(); // Editor ID column - if (ImGui::Selectable(editorLabel.c_str(), widgets[i]->IsOpen(), ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + bool isSelected = sortedWidgets[i]->IsOpen(); + if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { - widgets[i]->SetOpen(true); + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); } } + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); + } // Opens a context menu on right click to mark records by color - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", widgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; for (auto& recordMarker : settings.recordMarkers) { @@ -226,11 +469,11 @@ void EditorWindow::ShowObjectsWindow() // Form ID column ImGui::TableNextColumn(); - ImGui::Text(widgets[i]->GetFormID().c_str()); + ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); // File column ImGui::TableNextColumn(); - ImGui::Text(widgets[i]->GetFilename().c_str()); + ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); // Status column ImGui::TableNextColumn(); @@ -284,6 +527,39 @@ void EditorWindow::ShowViewportWindow() void EditorWindow::ShowWidgetWindow() { + // Global shortcut for closing focused widget (Ctrl+W) + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_W, false)) { + // Close the most recently focused widget + for (int i = 0; i < (int)weatherWidgets.size(); i++) { + auto widget = weatherWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + for (int i = 0; i < (int)worldSpaceWidgets.size(); i++) { + auto widget = worldSpaceWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + for (int i = 0; i < (int)lightingTemplateWidgets.size(); i++) { + auto widget = lightingTemplateWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + for (int i = 0; i < (int)imageSpaceWidgets.size(); i++) { + auto widget = imageSpaceWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + } + for (int i = 0; i < (int)weatherWidgets.size(); i++) { auto widget = weatherWidgets[i]; if (widget->IsOpen()) @@ -408,6 +684,12 @@ void EditorWindow::RenderUI() if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Automatically apply weather changes to the game as you edit"); } + if (ImGui::Checkbox("Remember Open Widgets", &settings.rememberOpenWidgets)) { + Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Restore previously open widgets when editor reopens"); + } ImGui::EndMenu(); } if (ImGui::BeginMenu("Window")) { @@ -457,20 +739,66 @@ void EditorWindow::RenderUI() if (ImGui::BeginMenu("Help")) { ImGui::Text("Weather Editor"); ImGui::Separator(); - ImGui::BulletText("Double-click objects to edit"); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Keyboard Shortcuts:"); + ImGui::BulletText("Ctrl+F: Focus search"); + ImGui::BulletText("Ctrl+S: Save all open widgets"); + ImGui::BulletText("Ctrl+W: Close focused widget"); + ImGui::BulletText("Enter: Open selected widget"); + ImGui::BulletText("Esc: Close editor"); + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Quick Tips:"); + ImGui::BulletText("Double-click to edit"); ImGui::BulletText("Right-click to mark status"); + ImGui::BulletText("Click star icon to favorite"); + ImGui::BulletText("Use quick filters for fast sorting"); ImGui::BulletText("Auto-Apply updates game live"); ImGui::BulletText("Lock weather to prevent changes"); - ImGui::BulletText("Changes save to JSON files"); ImGui::Separator(); ImGui::Text("Total Objects:"); ImGui::BulletText("Weathers: %d", (int)weatherWidgets.size()); ImGui::BulletText("WorldSpaces: %d", (int)worldSpaceWidgets.size()); ImGui::BulletText("Lighting: %d", (int)lightingTemplateWidgets.size()); ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); + ImGui::Separator(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Favorites: %d", (int)settings.favoriteWidgets.size()); + ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "Recent: %d", (int)settings.recentWidgets.size()); ImGui::EndMenu(); } + // Pause Time button + auto menu = globals::menu; + if (menu && menu->uiIcons.pauseTime.texture) { + bool isPaused = IsTimePaused(); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + if (isPaused) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.1f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.15f, 0.6f, 0.15f, 1.0f)); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + } + + const float menuBarHeight = ImGui::GetFrameHeight(); + const float buttonDim = menuBarHeight * 0.85f; // 85% of menu bar height + const ImVec2 buttonSize(buttonDim, buttonDim); + + if (ImGui::ImageButton("##GlobalPauseTime", menu->uiIcons.pauseTime.texture, buttonSize)) { + if (isPaused) { + ResumeTime(); + } else { + PauseTime(); + } + } + + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); + } + } + // Weather lock indicator if (weatherLockActive && lockedWeather) { ImGui::SameLine(); @@ -576,11 +904,13 @@ void EditorWindow::Draw() static bool wasOpen = false; if (open && !wasOpen) { - // Editor just opened - disable vanity camera + // Editor just opened - disable vanity camera and restore session DisableVanityCamera(); + RestoreSessionWidgets(); } else if (!open && wasOpen) { - // Editor just closed - restore vanity camera + // Editor just closed - restore vanity camera and save session RestoreVanityCamera(); + SaveSessionWidgets(); } wasOpen = open; @@ -671,7 +1001,7 @@ void EditorWindow::ShowSettingsWindow() // Left column: Options ImGui::TableSetColumnIndex(0); // List of options - const char* options[] = { "Flags" }; + const char* options[] = { "General", "Flags" }; for (int i = 0; i < IM_ARRAYSIZE(options); ++i) { if (ImGui::Selectable(options[i], selectedOption == options[i])) { selectedOption = options[i]; // Update selected option @@ -681,8 +1011,37 @@ void EditorWindow::ShowSettingsWindow() // Right column: Option settings ImGui::TableSetColumnIndex(1); - // Create a table for the right column. - if (selectedOption == "Flags") { + if (selectedOption == "General") { + ImGui::Checkbox("Auto-apply changes", &settings.autoApplyChanges); + AddTooltip("Automatically apply changes to weather/lighting when editing"); + + ImGui::Checkbox("Suppress delete warnings", &settings.suppressDeleteWarning); + AddTooltip("Don't show confirmation dialog when deleting saved files"); + + ImGui::Checkbox("Use text buttons instead of icons", &settings.useTextButtons); + AddTooltip("Display action buttons as text labels instead of icons"); + + ImGui::Separator(); + ImGui::TextUnformatted("Session & History"); + ImGui::Spacing(); + + ImGui::Checkbox("Remember open widgets", &settings.rememberOpenWidgets); + AddTooltip("Automatically reopen widgets that were open when you last closed the editor"); + + ImGui::SliderInt("Max recent widgets", &settings.maxRecentWidgets, 5, 20); + AddTooltip("Maximum number of recent widgets to remember"); + + if (ImGui::Button("Clear Recent History")) { + settings.recentWidgets.clear(); + Save(); + } + ImGui::SameLine(); + if (ImGui::Button("Clear Favorites")) { + settings.favoriteWidgets.clear(); + Save(); + } + + } else if (selectedOption == "Flags") { if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Colour", ImGuiTableColumnFlags_WidthStretch); @@ -697,25 +1056,30 @@ void EditorWindow::ShowSettingsWindow() // Store rename info (old name -> new name) static std::pair renameInfo; static bool needsRename = false; + + // Store separate buffers for each marker + static std::unordered_map> labelBuffers; for (auto& recordMarker : recordMarkers) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - // Editable label - static char labelBuffer[256]; - strncpy_s(labelBuffer, recordMarker.first.c_str(), sizeof(labelBuffer) - 1); - labelBuffer[sizeof(labelBuffer) - 1] = '\0'; + // Editable label - use separate buffer for each marker + auto& labelBuffer = labelBuffers[recordMarker.first]; + if (labelBuffer[0] == '\0' || labelBuffers.find(recordMarker.first) == labelBuffers.end()) { + strncpy_s(labelBuffer.data(), labelBuffer.size(), recordMarker.first.c_str(), labelBuffer.size() - 1); + labelBuffer[labelBuffer.size() - 1] = '\0'; + } ImGui::SetNextItemWidth(-1); - if (ImGui::InputText(std::format("##Label{}", recordMarker.first).c_str(), labelBuffer, sizeof(labelBuffer))) { - // Mark for rename - renameInfo = { recordMarker.first, std::string(labelBuffer) }; + if (ImGui::InputText(std::format("##Label{}", recordMarker.first).c_str(), labelBuffer.data(), labelBuffer.size(), ImGuiInputTextFlags_EnterReturnsTrue)) { + // Mark for rename only on Enter + renameInfo = { recordMarker.first, std::string(labelBuffer.data()) }; needsRename = true; } ImGui::TableSetColumnIndex(1); - if (ImGui::ColorEdit4(std::format("Color##{}", recordMarker.first).c_str(), (float*)&recordMarker.second)) { + if (ImGui::ColorEdit3(std::format("Color##{}", recordMarker.first).c_str(), (float*)&recordMarker.second)) { Save(); } @@ -975,3 +1339,92 @@ void EditorWindow::RenderNotifications() ImGui::PopStyleVar(2); } } + +void EditorWindow::AddToRecent(const std::string& widgetId) +{ + // Remove if already exists + auto it = std::find(settings.recentWidgets.begin(), settings.recentWidgets.end(), widgetId); + if (it != settings.recentWidgets.end()) { + settings.recentWidgets.erase(it); + } + + // Add to front + settings.recentWidgets.insert(settings.recentWidgets.begin(), widgetId); + + // Limit size + if (settings.recentWidgets.size() > static_cast(settings.maxRecentWidgets)) { + settings.recentWidgets.resize(settings.maxRecentWidgets); + } + + SaveSettings(); +} + +void EditorWindow::ToggleFavorite(const std::string& widgetId) +{ + auto it = std::find(settings.favoriteWidgets.begin(), settings.favoriteWidgets.end(), widgetId); + if (it != settings.favoriteWidgets.end()) { + settings.favoriteWidgets.erase(it); + } else { + settings.favoriteWidgets.push_back(widgetId); + } + SaveSettings(); +} + +bool EditorWindow::IsFavorite(const std::string& widgetId) const +{ + return std::find(settings.favoriteWidgets.begin(), settings.favoriteWidgets.end(), widgetId) != settings.favoriteWidgets.end(); +} + +void EditorWindow::SaveSessionWidgets() +{ + settings.lastOpenWidgets.clear(); + + // Save all currently open widgets + for (auto widget : weatherWidgets) { + if (widget->IsOpen()) { + settings.lastOpenWidgets.push_back(widget->GetEditorID()); + } + } + for (auto widget : worldSpaceWidgets) { + if (widget->IsOpen()) { + settings.lastOpenWidgets.push_back(widget->GetEditorID()); + } + } + for (auto widget : lightingTemplateWidgets) { + if (widget->IsOpen()) { + settings.lastOpenWidgets.push_back(widget->GetEditorID()); + } + } + + SaveSettings(); +} + +void EditorWindow::RestoreSessionWidgets() +{ + if (!settings.rememberOpenWidgets || settings.lastOpenWidgets.empty()) { + return; + } + + // Open widgets that were open in last session + for (const auto& widgetId : settings.lastOpenWidgets) { + // Search in all widget collections + for (auto widget : weatherWidgets) { + if (widget->GetEditorID() == widgetId) { + widget->SetOpen(true); + break; + } + } + for (auto widget : worldSpaceWidgets) { + if (widget->GetEditorID() == widgetId) { + widget->SetOpen(true); + break; + } + } + for (auto widget : lightingTemplateWidgets) { + if (widget->GetEditorID() == widgetId) { + widget->SetOpen(true); + break; + } + } + } +} diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index 97545ee68c..f735193e71 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -87,11 +87,22 @@ class EditorWindow std::map markedRecords; bool autoApplyChanges = true; bool suppressDeleteWarning = false; + bool useTextButtons = false; + std::vector favoriteWidgets; + std::vector recentWidgets; + int maxRecentWidgets = 10; + bool rememberOpenWidgets = true; + std::vector lastOpenWidgets; }; Settings settings; void Save(); + void AddToRecent(const std::string& widgetId); + void ToggleFavorite(const std::string& widgetId); + bool IsFavorite(const std::string& widgetId) const; + void SaveSessionWidgets(); + void RestoreSessionWidgets(); private: void SaveAll(); @@ -102,4 +113,9 @@ class EditorWindow json j; std::string settingsFilename = "EditorSettings"; bool showSettingsWindow = false; + + // Sorting state + enum class SortColumn { None, EditorID, FormID, File, Status }; + SortColumn currentSortColumn = SortColumn::None; + bool sortAscending = true; }; \ No newline at end of file diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp index abded0654b..2ae84739c7 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -1,6 +1,7 @@ #include "ImageSpaceWidget.h" #include "../EditorWindow.h" +#include "../WeatherUtils.h" #include "Util.h" #include @@ -39,23 +40,188 @@ void ImageSpaceWidget::DrawWidget() ImGui::Text("Plugin: %s", GetFilename().c_str()); ImGui::Separator(); - // Draw settings sections - DrawHDRSettings(); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); + // Search bar (activatable with Ctrl+F) + BeginWidgetSearchBar(searchBuffer, sizeof(searchBuffer), searchActive); + + // Draw all settings in a unified table + if (ImGui::BeginTable("ImageSpaceSettings", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + bool changed = false; + + // HDR Settings + if (MatchesSearch("Eye Adapt Speed")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Eye Adapt Speed"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##EyeAdaptSpeed", &settings.hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f")) + changed = true; + } - DrawCinematicSettings(); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); + if (MatchesSearch("Bloom Blur Radius")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Bloom Blur Radius"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##BloomBlurRadius", &settings.hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f")) + changed = true; + } - DrawTintSettings(); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); + if (MatchesSearch("Bloom Threshold")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Bloom Threshold"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##BloomThreshold", &settings.hdrBloomThreshold, 0.0f, 10.0f, "%.3f")) + changed = true; + } + + if (MatchesSearch("Bloom Scale")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Bloom Scale"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##BloomScale", &settings.hdrBloomScale, 0.0f, 10.0f, "%.3f")) + changed = true; + } + + if (MatchesSearch("Sunlight Scale")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Sunlight Scale"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##SunlightScale", &settings.hdrSunlightScale, 0.0f, 10.0f, "%.3f")) + changed = true; + } + + if (MatchesSearch("Sky Scale")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Sky Scale"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##SkyScale", &settings.hdrSkyScale, 0.0f, 10.0f, "%.3f")) + changed = true; + } + + // Separator between sections + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + // Cinematic Settings + if (MatchesSearch("Saturation")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Saturation"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##Saturation", &settings.cinematicSaturation, 0.0f, 2.0f, "%.3f")) + changed = true; + } + + if (MatchesSearch("Brightness")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Brightness"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##Brightness", &settings.cinematicBrightness, 0.0f, 2.0f, "%.3f")) + changed = true; + } + + if (MatchesSearch("Contrast")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Contrast"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##Contrast", &settings.cinematicContrast, 0.0f, 2.0f, "%.3f")) + changed = true; + } + + // Separator + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + // Tint Settings + if (MatchesSearch("Tint Color")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Tint Color"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::ColorEdit3("##TintColor", &settings.tintColor.x)) + changed = true; + } + + if (MatchesSearch("Tint Amount")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Tint Amount"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##TintAmount", &settings.tintAmount, 0.0f, 1.0f, "%.3f")) + changed = true; + } + + // Separator + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + // Depth of Field + if (MatchesSearch("DOF Strength")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("DOF Strength"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##DOFStrength", &settings.dofStrength, 0.0f, 10.0f, "%.3f")) + changed = true; + } + + if (MatchesSearch("DOF Distance")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("DOF Distance"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##DOFDistance", &settings.dofDistance, 0.0f, 10000.0f, "%.1f")) + changed = true; + } + + if (MatchesSearch("DOF Range")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("DOF Range"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##DOFRange", &settings.dofRange, 0.0f, 10000.0f, "%.1f")) + changed = true; + } + + ImGui::EndTable(); + + if (changed && editorWindow->settings.autoApplyChanges) { + ApplyChanges(); + } + } - DrawDOFSettings(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -127,60 +293,6 @@ void ImageSpaceWidget::DrawWidget() ImGui::End(); } -void ImageSpaceWidget::DrawHDRSettings() -{ - if (ImGui::CollapsingHeader("HDR Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Eye Adapt Speed", &settings.hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f"); - ImGui::SliderFloat("Bloom Blur Radius", &settings.hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f"); - ImGui::SliderFloat("Bloom Threshold", &settings.hdrBloomThreshold, 0.0f, 10.0f, "%.3f"); - ImGui::SliderFloat("Bloom Scale", &settings.hdrBloomScale, 0.0f, 10.0f, "%.3f"); - ImGui::SliderFloat("Sunlight Scale", &settings.hdrSunlightScale, 0.0f, 10.0f, "%.3f"); - ImGui::SliderFloat("Sky Scale", &settings.hdrSkyScale, 0.0f, 10.0f, "%.3f"); - - if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } - } -} - -void ImageSpaceWidget::DrawCinematicSettings() -{ - if (ImGui::CollapsingHeader("Cinematic Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Saturation", &settings.cinematicSaturation, 0.0f, 2.0f, "%.3f"); - ImGui::SliderFloat("Brightness", &settings.cinematicBrightness, 0.0f, 2.0f, "%.3f"); - ImGui::SliderFloat("Contrast", &settings.cinematicContrast, 0.0f, 2.0f, "%.3f"); - - if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } - } -} - -void ImageSpaceWidget::DrawTintSettings() -{ - if (ImGui::CollapsingHeader("Tint Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::ColorEdit3("Tint Color", &settings.tintColor.x); - ImGui::SliderFloat("Tint Amount", &settings.tintAmount, 0.0f, 1.0f, "%.3f"); - - if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } - } -} - -void ImageSpaceWidget::DrawDOFSettings() -{ - if (ImGui::CollapsingHeader("Depth of Field", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("DOF Strength", &settings.dofStrength, 0.0f, 10.0f, "%.3f"); - ImGui::SliderFloat("DOF Distance", &settings.dofDistance, 0.0f, 10000.0f, "%.1f"); - ImGui::SliderFloat("DOF Range", &settings.dofRange, 0.0f, 10000.0f, "%.1f"); - - if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } - } -} - void ImageSpaceWidget::LoadSettings() { try { diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.h b/src/WeatherEditor/Weather/ImageSpaceWidget.h index ee506fc57b..0a718dacca 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.h +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.h @@ -51,10 +51,4 @@ class ImageSpaceWidget : public Widget void LoadImageSpaceValues(); void ApplyChanges(); void RevertChanges(); - -private: - void DrawHDRSettings(); - void DrawCinematicSettings(); - void DrawTintSettings(); - void DrawDOFSettings(); }; diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index 13fa16e931..0bac195792 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -36,7 +36,7 @@ void LightingTemplateWidget::DrawWidget() if (!editorWindow->settings.autoApplyChanges) { auto menu = globals::menu; - bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && + bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons && menu->uiIcons.saveSettings.texture && menu->uiIcons.featureSettingRevert.texture; @@ -67,17 +67,29 @@ void LightingTemplateWidget::DrawWidget() ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } else { + const float buttonHeight = ImGui::GetFrameHeight(); + + ImVec2 applySize = ImGui::CalcTextSize("Apply"); + applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + applySize.y = buttonHeight; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { + if (ImGui::Button("Apply", applySize)) { ApplyChanges(); } ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Apply changes to the game"); } + ImGui::SameLine(); + + ImVec2 revertSize = ImGui::CalcTextSize("Revert"); + revertSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + revertSize.y = buttonHeight; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - if (ImGui::Button("Revert", ImVec2(-1, 0))) { + if (ImGui::Button("Revert", revertSize)) { RevertChanges(); } ImGui::PopStyleColor(); @@ -88,6 +100,9 @@ void LightingTemplateWidget::DrawWidget() } ImGui::Separator(); + // Search bar (activatable with Ctrl+F) + BeginWidgetSearchBar(searchBuffer, sizeof(searchBuffer), searchActive); + if (ImGui::BeginTabBar("LightingTemplateSettingsTabs", ImGuiTabBarFlags_None)) { if (ImGui::BeginTabItem("Basic")) { DrawBasicSettings(); @@ -116,34 +131,34 @@ void LightingTemplateWidget::DrawBasicSettings() if (ImGui::CollapsingHeader("Ambient & Directional", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawColorEdit("Ambient Color", settings.ambient)) changed = true; - ImGui::Spacing(); - if (DrawColorEdit("Directional Color", settings.directional)) changed = true; - ImGui::Spacing(); + if (MatchesSearch("Ambient Color") && DrawColorEdit("Ambient Color", settings.ambient)) changed = true; + if (MatchesSearch("Ambient Color")) ImGui::Spacing(); + if (MatchesSearch("Directional Color") && DrawColorEdit("Directional Color", settings.directional)) changed = true; + if (MatchesSearch("Directional Color")) ImGui::Spacing(); } if (ImGui::CollapsingHeader("Directional Settings", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; - ImGui::Spacing(); + if (MatchesSearch("Directional XY") && DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; + if (MatchesSearch("Directional XY")) ImGui::Spacing(); + if (MatchesSearch("Directional Z") && DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; + if (MatchesSearch("Directional Z")) ImGui::Spacing(); + if (MatchesSearch("Directional Fade") && DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; + if (MatchesSearch("Directional Fade")) ImGui::Spacing(); } if (ImGui::CollapsingHeader("Light Fade", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; - ImGui::Spacing(); + if (MatchesSearch("Light Fade Start") && DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; + if (MatchesSearch("Light Fade Start")) ImGui::Spacing(); + if (MatchesSearch("Light Fade End") && DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; + if (MatchesSearch("Light Fade End")) ImGui::Spacing(); } if (ImGui::CollapsingHeader("Other", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; - ImGui::Spacing(); + if (MatchesSearch("Clip Distance") && DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; + if (MatchesSearch("Clip Distance")) ImGui::Spacing(); } if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { @@ -156,22 +171,22 @@ void LightingTemplateWidget::DrawFogSettings() bool changed = false; ImGui::Spacing(); - if (DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; - ImGui::Spacing(); - if (DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; - ImGui::Spacing(); + if (MatchesSearch("Fog Color Near") && DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; + if (MatchesSearch("Fog Color Near")) ImGui::Spacing(); + if (MatchesSearch("Fog Color Far") && DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; + if (MatchesSearch("Fog Color Far")) ImGui::Spacing(); ImGui::Spacing(); - if (DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; - ImGui::Spacing(); + if (MatchesSearch("Fog Near") && DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; + if (MatchesSearch("Fog Near")) ImGui::Spacing(); + if (MatchesSearch("Fog Far") && DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; + if (MatchesSearch("Fog Far")) ImGui::Spacing(); ImGui::Spacing(); - if (DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; - ImGui::Spacing(); + if (MatchesSearch("Fog Power") && DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; + if (MatchesSearch("Fog Power")) ImGui::Spacing(); + if (MatchesSearch("Fog Clamp") && DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; + if (MatchesSearch("Fog Clamp")) ImGui::Spacing(); if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); @@ -182,21 +197,62 @@ void LightingTemplateWidget::DrawDALCSettings() { bool changed = false; - ImGui::Spacing(); - if (DrawColorEdit("Specular", settings.dalc.specular)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; - ImGui::Spacing(); - - for (int j = 0; j < 3; j++) { - const char* labels[] = { "X", "Y", "Z" }; - ImGui::Separator(); - ImGui::Text("Directional %s", labels[j]); - if (DrawColorEdit(std::format("Max##{}", labels[j]), settings.dalc.directional[j].max)) changed = true; - if (DrawColorEdit(std::format("Min##{}", labels[j]), settings.dalc.directional[j].min)) changed = true; + if (ImGui::CollapsingHeader("Basic DALC", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Spacing(); + if (DrawColorEdit("Specular", settings.dalc.specular)) changed = true; + ImGui::Spacing(); + if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; ImGui::Spacing(); } + if (ImGui::CollapsingHeader("Directional Colors (Time of Day)", ImGuiTreeNodeFlags_DefaultOpen)) { + // Note: The game engine uses X, Y, Z to represent time periods + // We map them to: X=Sunrise/Day, Y=Sunset, Z=Night for TOD display + + if (TOD::BeginTODTable("DALCDirectionalTable")) { + TOD::RenderTODHeader(); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + // Prepare arrays for TOD rendering (map X,Y,Z to Sunrise,Day,Sunset,Night) + float3 maxColors[4]; + float3 minColors[4]; + + // Map X (index 0) to Sunrise and Day + maxColors[TOD::Sunrise] = settings.dalc.directional[0].max; + maxColors[TOD::Day] = settings.dalc.directional[0].max; + minColors[TOD::Sunrise] = settings.dalc.directional[0].min; + minColors[TOD::Day] = settings.dalc.directional[0].min; + + // Map Y (index 1) to Sunset + maxColors[TOD::Sunset] = settings.dalc.directional[1].max; + minColors[TOD::Sunset] = settings.dalc.directional[1].min; + + // Map Z (index 2) to Night + maxColors[TOD::Night] = settings.dalc.directional[2].max; + minColors[TOD::Night] = settings.dalc.directional[2].min; + + if (TOD::DrawTODColorRow("Max", maxColors)) { + settings.dalc.directional[0].max = maxColors[TOD::Sunrise]; // X from Sunrise + settings.dalc.directional[1].max = maxColors[TOD::Sunset]; // Y from Sunset + settings.dalc.directional[2].max = maxColors[TOD::Night]; // Z from Night + changed = true; + } + + if (TOD::DrawTODColorRow("Min", minColors)) { + settings.dalc.directional[0].min = minColors[TOD::Sunrise]; // X from Sunrise + settings.dalc.directional[1].min = minColors[TOD::Sunset]; // Y from Sunset + settings.dalc.directional[2].min = minColors[TOD::Night]; // Z from Night + changed = true; + } + + TOD::EndTODTable(); + } + } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } @@ -285,6 +341,8 @@ void LightingTemplateWidget::LoadSettings() { if (!js.empty()) { settings = js; + } else { + LoadLightingTemplateValues(); } } diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 88e1236688..236618d793 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -50,45 +50,8 @@ void WeatherWidget::DrawWidget() auto editorWindow = EditorWindow::GetSingleton(); bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; - // Weather lock and time controls (inline) - float buttonWidth = ImGui::GetContentRegionAvail().x * 0.49f; - - // Weather lock controls - if (isLocked) { - if (ImGui::Button("Unlock Weather", ImVec2(buttonWidth, 0))) { - editorWindow->UnlockWeather(); - } - } else { - if (ImGui::Button("Lock & Force This Weather", ImVec2(buttonWidth, 0))) { - editorWindow->LockWeather(weather); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Force this weather to be active and prevent time-based weather changes"); - } - } - - ImGui::SameLine(); - - // Time pause toggle - bool isPaused = editorWindow->IsTimePaused(); - if (isPaused) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); - if (ImGui::Button("Resume Time", ImVec2(-1, 0))) { - editorWindow->ResumeTime(); - } - ImGui::PopStyleColor(2); - } else { - if (ImGui::Button("Pause Time", ImVec2(-1, 0))) { - editorWindow->PauseTime(); - } - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Pause/resume time progression in game"); - } - auto menu = globals::menu; - bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && + bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons && menu->uiIcons.applyToGame.texture && menu->uiIcons.saveSettings.texture && menu->uiIcons.loadSettings.texture; @@ -96,7 +59,33 @@ void WeatherWidget::DrawWidget() if (useIcons) { const float iconSize = ImGui::GetFrameHeight(); const ImVec2 buttonSize(iconSize, iconSize); + + // Lock/Unlock text button + const char* lockLabel = isLocked ? "Unlock" : "Force Weather"; + ImVec2 lockTextSize = ImGui::CalcTextSize(lockLabel); + float lockButtonWidth = lockTextSize.x + ImGui::GetStyle().FramePadding.x * 2.0f; + + if (isLocked) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); + } + if (ImGui::Button(lockLabel, ImVec2(lockButtonWidth, iconSize))) { + if (isLocked) { + editorWindow->UnlockWeather(); + } else { + editorWindow->LockWeather(weather); + } + } + if (isLocked) { + ImGui::PopStyleColor(2); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isLocked ? "Unlock Weather" : "Force This Weather"); + } + ImGui::SameLine(); + + // Icon buttons with transparent background ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); @@ -127,38 +116,71 @@ void WeatherWidget::DrawWidget() Load(); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Load from file"); + ImGui::SetTooltip("Load saved file (or reset to vanilla if no file)"); } - ImGui::SameLine(); + // Delete button - only visible if a saved file exists + if (HasSavedFile()) { + ImGui::SameLine(); - // Delete button - always visible - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); - if (ImGui::ImageButton("##DeleteWeather", menu->uiIcons.deleteSettings.texture, buttonSize)) { - if (editorWindow->settings.suppressDeleteWarning) { - Delete(); - } else { - ImGui::OpenPopup("ConfirmDelete"); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::ImageButton("##DeleteWeather", menu->uiIcons.deleteSettings.texture, buttonSize)) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + ImGui::OpenPopup("ConfirmDelete"); + } + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete saved file and revert to defaults"); } - } - ImGui::PopStyleColor(2); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Delete saved file and revert to defaults"); } ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } else { - // Text-based fallback buttons - bool hasApplyButton = !editorWindow->settings.autoApplyChanges; - int buttonCount = hasApplyButton ? 4 : 3; - float textButtonWidth = ImGui::GetContentRegionAvail().x / buttonCount; + // Text-based buttons - all inline with wrapping support + const float buttonHeight = ImGui::GetFrameHeight(); + const float spacing = ImGui::GetStyle().ItemSpacing.x; + + // Lock/Unlock button + const char* lockLabel = isLocked ? "Unlock" : "Lock"; + ImVec2 lockSize = ImGui::CalcTextSize(lockLabel); + lockSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + lockSize.y = buttonHeight; + + if (isLocked) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); + } + if (ImGui::Button(lockLabel, lockSize)) { + if (isLocked) { + editorWindow->UnlockWeather(); + } else { + editorWindow->LockWeather(weather); + } + } + if (isLocked) { + ImGui::PopStyleColor(2); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isLocked ? "Unlock Weather" : "Lock & Force This Weather"); + } + + ImGui::SameLine(); + ImGui::Dummy(ImVec2(spacing * 2.0f, 0)); + ImGui::SameLine(); // Apply button - only show when auto-apply is disabled - if (hasApplyButton) { + if (!editorWindow->settings.autoApplyChanges) { + ImVec2 applySize = ImGui::CalcTextSize("Apply"); + applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + applySize.y = buttonHeight; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", ImVec2(textButtonWidth * 0.97f, 0))) { + if (ImGui::Button("Apply", applySize)) { ApplyChanges(); } ImGui::PopStyleColor(); @@ -168,8 +190,12 @@ void WeatherWidget::DrawWidget() ImGui::SameLine(); } - // Save button - always visible - if (ImGui::Button("Save", ImVec2(textButtonWidth * 0.97f, 0))) { + // Save button + ImVec2 saveSize = ImGui::CalcTextSize("Save"); + saveSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + saveSize.y = buttonHeight; + + if (ImGui::Button("Save", saveSize)) { Save(); } if (ImGui::IsItemHovered()) { @@ -178,30 +204,39 @@ void WeatherWidget::DrawWidget() ImGui::SameLine(); - // Load button - always visible - if (ImGui::Button("Load", ImVec2(textButtonWidth * 0.97f, 0))) { + // Load button + ImVec2 loadSize = ImGui::CalcTextSize("Load"); + loadSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + loadSize.y = buttonHeight; + + if (ImGui::Button("Load", loadSize)) { Load(); } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Load from file"); + ImGui::SetTooltip("Load saved file (or reset to vanilla if no file)"); } - ImGui::SameLine(); - - // Delete button - always visible - float deleteIconSize = ImGui::GetFrameHeight(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); - if (ImGui::ImageButton("##DeleteWeatherText", menu->uiIcons.deleteSettings.texture, ImVec2(deleteIconSize, deleteIconSize))) { - if (editorWindow->settings.suppressDeleteWarning) { - Delete(); - } else { - ImGui::OpenPopup("ConfirmDelete"); + // Delete button - only visible if a saved file exists + if (HasSavedFile()) { + ImGui::SameLine(); + + ImVec2 deleteSize = ImGui::CalcTextSize("Delete"); + deleteSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + deleteSize.y = buttonHeight; + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::Button("Delete", deleteSize)) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + ImGui::OpenPopup("ConfirmDelete"); + } + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete saved file and revert to defaults"); } - } - ImGui::PopStyleColor(2); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Delete saved file and revert to defaults"); } } @@ -234,6 +269,51 @@ void WeatherWidget::DrawWidget() ImGui::Separator(); + // Search bar with dropdown + ImGui::SetNextItemWidth(200.0f); + if (ImGui::InputTextWithHint("##WeatherSearch", "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer))) { + UpdateSearchResults(); + } + + // Handle Ctrl+F to focus search + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(-1); + } + + // Show search results dropdown + if (searchBuffer[0] != '\0' && !searchResults.empty()) { + ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMax().y)); + ImGui::SetNextWindowSize(ImVec2(ImGui::GetItemRectSize().x * 1.5f, 0)); + ImGui::SetNextWindowFocus(); + if (ImGui::Begin("##SearchDropdown", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { + + for (size_t i = 0; i < std::min(size_t(5), searchResults.size()); ++i) { + const auto& result = searchResults[i]; + std::string label = std::format("{} ({})", result.displayName, result.tabName); + + if (ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_DontClosePopups)) { + NavigateToSetting(result); + searchBuffer[0] = '\0'; + searchResults.clear(); + } + } + + if (searchResults.size() > 5) { + ImGui::Separator(); + ImGui::TextDisabled("... %zu more results", searchResults.size() - 5); + } + + // Close dropdown if clicking outside or pressing Escape + if (!ImGui::IsWindowFocused() || ImGui::IsKeyPressed(ImGuiKey_Escape)) { + searchBuffer[0] = '\0'; + searchResults.clear(); + } + } + ImGui::End(); + } + auto& widgets = editorWindow->weatherWidgets; // Sets the parent widget if settings have been loaded. @@ -279,7 +359,19 @@ void WeatherWidget::DrawWidget() // Tab bar for organizing settings if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { - if (ImGui::BeginTabItem("Basic")) { + // Use activeTabOverride to auto-navigate to specific tab + ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags dalcFlags = (activeTabOverride == "Lighting (DALC)") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags atmosphereFlags = (activeTabOverride == "Atmosphere Colors") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags imageSpaceFlags = (activeTabOverride == "ImageSpace") ? ImGuiTabItemFlags_SetSelected : 0; + if (!activeTabOverride.empty()) { + activeTabOverride = ""; // Clear after use + } + + if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { DrawProperties("Sun", { { "Sun Glare", INT8_SLIDER }, { "Sun Damage", INT8_SLIDER } }); DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); @@ -290,33 +382,32 @@ void WeatherWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Lighting (DALC)")) { + if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { DrawDALCSettings(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Atmosphere Colors")) { + if (ImGui::BeginTabItem("Atmosphere Colors", nullptr, atmosphereFlags)) { DrawWeatherColorSettings(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Clouds")) { + if (ImGui::BeginTabItem("Clouds", nullptr, cloudsFlags)) { DrawCloudSettings(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Fog")) { - DrawProperties("Fog", { { "Day Near", FLOAT_SLIDER }, { "Day Far", FLOAT_SLIDER }, { "Day Power", FLOAT_SLIDER }, { "Day Max", FLOAT_SLIDER }, - { "Night Near", FLOAT_SLIDER }, { "Night Far", FLOAT_SLIDER }, { "Night Power", FLOAT_SLIDER }, { "Night Max", FLOAT_SLIDER } }); + if (ImGui::BeginTabItem("Fog", nullptr, fogFlags)) { + DrawFogSettings(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Features")) { + if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { DrawFeatureSettings(); ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("ImageSpace")) { + if (ImGui::BeginTabItem("ImageSpace", nullptr, imageSpaceFlags)) { DrawImageSpaceSettings(); ImGui::EndTabItem(); } @@ -377,9 +468,9 @@ void WeatherWidget::LoadSettings() return; } } else { - // No JSON data, load vanilla values - logger::info("Weather {}: No JSON data, loading vanilla values", GetEditorID()); - LoadWeatherValues(); + // No JSON data, restore cached vanilla values + logger::info("Weather {}: No JSON data, restoring cached vanilla values", GetEditorID()); + settings = vanillaSettings; } LoadFeatureSettings(); } @@ -625,22 +716,88 @@ void WeatherWidget::DrawDALCSettings() } else { doesInherit = false; bool changed = false; - for (int i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { - std::string label = ColorTimeLabel(i); - if (ImGui::CollapsingHeader(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - ImGui::Spacing(); - if (DrawColorEdit(std::format("Specular##{}", label), settings.dalc[i].specular)) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat(std::format("Fresnel Power##{}", label), settings.dalc[i].fresnelPower)) changed = true; - ImGui::Spacing(); + if (TOD::BeginTODTable("DALC_TOD_Table")) { + TOD::RenderTODHeader(); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); - for (int j = 0; j < 3; j++) { - if (DrawColorEdit(std::format("DALC X Max##{}", label), settings.dalc[i].directional[j].max)) changed = true; - if (DrawColorEdit(std::format("DALC X Min##{}", label), settings.dalc[i].directional[j].min)) changed = true; - ImGui::Spacing(); - } + // Prepare arrays for TOD rendering + float3 specularColors[4]; + float fresnelPowers[4]; + float3 directionalXMax[4], directionalXMin[4]; + float3 directionalYMax[4], directionalYMin[4]; + float3 directionalZMax[4], directionalZMin[4]; + + for (int i = 0; i < ColorTimes::kTotal; i++) { + specularColors[i] = settings.dalc[i].specular; + fresnelPowers[i] = settings.dalc[i].fresnelPower; + directionalXMax[i] = settings.dalc[i].directional[0].max; + directionalXMin[i] = settings.dalc[i].directional[0].min; + directionalYMax[i] = settings.dalc[i].directional[1].max; + directionalYMin[i] = settings.dalc[i].directional[1].min; + directionalZMax[i] = settings.dalc[i].directional[2].max; + directionalZMin[i] = settings.dalc[i].directional[2].min; } + + if (TOD::DrawTODColorRow("Specular", specularColors)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].specular = specularColors[i]; + changed = true; + } + + if (TOD::DrawTODSliderRow("Fresnel Power", fresnelPowers, 0.0f, 10.0f)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].fresnelPower = fresnelPowers[i]; + changed = true; + } + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + if (TOD::DrawTODColorRow("Directional X Max", directionalXMax)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[0].max = directionalXMax[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional X Min", directionalXMin)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[0].min = directionalXMin[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional Y Max", directionalYMax)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[1].max = directionalYMax[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional Y Min", directionalYMin)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[1].min = directionalYMin[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional Z Max", directionalZMax)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[2].max = directionalZMax[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional Z Min", directionalZMin)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[2].min = directionalZMin[i]; + changed = true; + } + + TOD::EndTODTable(); } if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); @@ -661,15 +818,23 @@ void WeatherWidget::DrawWeatherColorSettings() doesInherit = false; bool changed = false; - for (int i = 0; i < ColorTypes::kTotal; i++) { - std::string colorTypeLabel = ColorTypeLabel(i); + if (TOD::BeginTODTable("AtmosphereColors_Table")) { + TOD::RenderTODHeader(); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + for (int i = 0; i < ColorTypes::kTotal; i++) { + std::string colorTypeLabel = ColorTypeLabel(i); - if (ImGui::CollapsingHeader(colorTypeLabel.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - for (int j = 0; j < ColorTimes::kTotal; j++) { - if (DrawColorEdit(std::format("{}##{}", ColorTimeLabel(j), colorTypeLabel), settings.atmosphereColors[i].colorTimes[j])) changed = true; - ImGui::Spacing(); + if (TOD::DrawTODColorRow(colorTypeLabel.c_str(), settings.atmosphereColors[i].colorTimes)) { + changed = true; } } + + TOD::EndTODTable(); } if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { @@ -697,16 +862,25 @@ void WeatherWidget::DrawCloudSettings() if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; ImGui::Spacing(); if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; + ImGui::Spacing(); - for (int j = 0; j < ColorTimes::kTotal; j++) { - std::string colorTime = ColorTimeLabel(j).c_str(); + if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { + TOD::RenderTODHeader(); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); - if (ImGui::CollapsingHeader(colorTime.c_str(), ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - if (DrawColorEdit(std::format("Cloud Color##{}{}", colorTime, i), settings.clouds[i].color[j])) changed = true; - ImGui::Spacing(); - if (DrawSliderFloat(std::format("Cloud Alpha##{}{}", colorTime, i), settings.clouds[i].cloudAlpha[j])) changed = true; - ImGui::Spacing(); + if (TOD::DrawTODColorRow("Cloud Color", settings.clouds[i].color)) { + changed = true; } + + if (TOD::DrawTODSliderRow("Cloud Alpha", settings.clouds[i].cloudAlpha, 0.0f, 1.0f)) { + changed = true; + } + + TOD::EndTODTable(); } } } @@ -716,9 +890,121 @@ void WeatherWidget::DrawCloudSettings() } } +void WeatherWidget::DrawFogSettings() +{ + bool& doesInherit = settings.inheritance["Fog"]; + ImGui::Checkbox("Inherit From Parent##fog", &doesInherit); + + if (doesInherit && HasParent()) { + settings.fogProperties = GetParent()->settings.fogProperties; + } else { + doesInherit = false; + bool changed = false; + + if (ImGui::BeginTable("FogTable", 3, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthFixed, 250.0f); + ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthFixed, 250.0f); + + // Header row + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Day"); + ImGui::TableSetColumnIndex(2); + ImGui::Text("Night"); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + ImGui::TableSetColumnIndex(2); + ImGui::Separator(); + + // Near + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Near"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayNear", &settings.fogProperties["Day Near"], 0.0f, 50000.0f, "%.0f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightNear", &settings.fogProperties["Night Near"], 0.0f, 50000.0f, "%.0f")) + changed = true; + + // Far + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Far"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayFar", &settings.fogProperties["Day Far"], 0.0f, 50000.0f, "%.0f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightFar", &settings.fogProperties["Night Far"], 0.0f, 50000.0f, "%.0f")) + changed = true; + + // Power + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Power"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayPower", &settings.fogProperties["Day Power"], 0.0f, 50000.0f, "%.2f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightPower", &settings.fogProperties["Night Power"], 0.0f, 50000.0f, "%.2f")) + changed = true; + + // Max + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("Max"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayMax", &settings.fogProperties["Day Max"], 0.0f, 50000.0f, "%.2f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightMax", &settings.fogProperties["Night Max"], 0.0f, 50000.0f, "%.2f")) + changed = true; + + ImGui::EndTable(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } +} + void WeatherWidget::DrawProperties(std::string category, std::map properties) { bool& doesInherit = settings.inheritance[category]; + + // Check if any property matches search (only check if search is active) + bool hasMatchingProperty = false; + if (searchBuffer[0] != '\0') { + hasMatchingProperty = MatchesSearch(category); + if (!hasMatchingProperty) { + for (auto& p : properties) { + if (MatchesSearch(p.first)) { + hasMatchingProperty = true; + break; + } + } + } + // Skip this entire category if nothing matches + if (!hasMatchingProperty) { + return; + } + } + ImGui::Checkbox(std::format("Inherit From Parent##{}", category).c_str(), &doesInherit); if (doesInherit && HasParent()) { @@ -730,6 +1016,19 @@ void WeatherWidget::DrawProperties(std::string category, std::map(ImGui::GetTime()) - highlightStartTime; + float alpha = 0.3f * (1.0f - std::abs(elapsed - 0.5f) * 2.0f); // Fade in/out over 1 second + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.6f, 1.0f, alpha)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.7f, 1.0f, alpha)); + } + switch (p.second) { case 0: if (DrawSliderInt8(p.first, settings.weatherProperties[p.first])) changed = true; @@ -746,6 +1045,10 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.autoApplyChanges) { @@ -884,89 +1187,165 @@ void WeatherWidget::DrawImageSpaceSettings() ImGui::TextWrapped("Configure ImageSpace (post-processing) effects for different times of day."); ImGui::Spacing(); ImGui::Separator(); + ImGui::Spacing(); - const char* timeNames[] = { "Sunrise", "Day", "Sunset", "Night" }; + bool changed = false; - for (int timeIdx = 0; timeIdx < ColorTimes::kTotal; timeIdx++) { - auto& imgSpace = settings.imageSpaces[timeIdx]; - RE::TESImageSpace* weatherImgSpace = weather->imageSpaces[timeIdx]; + if (TOD::BeginTODTable("ImageSpace_TOD_Table")) { + TOD::RenderTODHeader(); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); - ImGui::PushID(timeIdx); + // Prepare arrays for TOD rendering + float hdrEyeAdaptSpeed[4]; + float hdrBloomBlurRadius[4]; + float hdrBloomThreshold[4]; + float hdrBloomScale[4]; + float hdrSunlightScale[4]; + float hdrSkyScale[4]; + float cinematicSaturation[4]; + float cinematicBrightness[4]; + float cinematicContrast[4]; + float3 tintColor[4]; + float tintAmount[4]; + float dofStrength[4]; + float dofDistance[4]; + float dofRange[4]; + + for (int i = 0; i < ColorTimes::kTotal; i++) { + hdrEyeAdaptSpeed[i] = settings.imageSpaces[i].hdrEyeAdaptSpeed; + hdrBloomBlurRadius[i] = settings.imageSpaces[i].hdrBloomBlurRadius; + hdrBloomThreshold[i] = settings.imageSpaces[i].hdrBloomThreshold; + hdrBloomScale[i] = settings.imageSpaces[i].hdrBloomScale; + hdrSunlightScale[i] = settings.imageSpaces[i].hdrSunlightScale; + hdrSkyScale[i] = settings.imageSpaces[i].hdrSkyScale; + cinematicSaturation[i] = settings.imageSpaces[i].cinematicSaturation; + cinematicBrightness[i] = settings.imageSpaces[i].cinematicBrightness; + cinematicContrast[i] = settings.imageSpaces[i].cinematicContrast; + tintColor[i] = settings.imageSpaces[i].tintColor; + tintAmount[i] = settings.imageSpaces[i].tintAmount; + dofStrength[i] = settings.imageSpaces[i].dofStrength; + dofDistance[i] = settings.imageSpaces[i].dofDistance; + dofRange[i] = settings.imageSpaces[i].dofRange; + } - if (ImGui::CollapsingHeader(timeNames[timeIdx], ImGuiTreeNodeFlags_DefaultOpen)) { - // Show which ImageSpace form is assigned - if (weatherImgSpace) { - ImGui::TextColored({ 0.7f, 0.7f, 0.7f, 1.0f }, "ImageSpace: %s (%08X)", - weatherImgSpace->GetFormEditorID(), - weatherImgSpace->GetFormID()); - } else { - ImGui::TextColored({ 1.0f, 0.5f, 0.0f, 1.0f }, "No ImageSpace assigned"); - } + // HDR Settings + if (TOD::DrawTODSliderRow("Eye Adapt Speed", hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].hdrEyeAdaptSpeed = hdrEyeAdaptSpeed[i]; + changed = true; + } - ImGui::Spacing(); + if (TOD::DrawTODSliderRow("Bloom Blur Radius", hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].hdrBloomBlurRadius = hdrBloomBlurRadius[i]; + changed = true; + } - // HDR Settings - if (ImGui::TreeNode("HDR Settings")) { - bool changed = false; - changed |= ImGui::SliderFloat("Eye Adapt Speed", &imgSpace.hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f"); - changed |= ImGui::SliderFloat("Bloom Blur Radius", &imgSpace.hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f"); - changed |= ImGui::SliderFloat("Bloom Threshold", &imgSpace.hdrBloomThreshold, 0.0f, 10.0f, "%.3f"); - changed |= ImGui::SliderFloat("Bloom Scale", &imgSpace.hdrBloomScale, 0.0f, 10.0f, "%.3f"); - changed |= ImGui::SliderFloat("Sunlight Scale", &imgSpace.hdrSunlightScale, 0.0f, 10.0f, "%.3f"); - changed |= ImGui::SliderFloat("Sky Scale", &imgSpace.hdrSkyScale, 0.0f, 10.0f, "%.3f"); - - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - SetImageSpaceValues(); - } + if (TOD::DrawTODSliderRow("Bloom Threshold", hdrBloomThreshold, 0.0f, 10.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].hdrBloomThreshold = hdrBloomThreshold[i]; + changed = true; + } - ImGui::TreePop(); - } + if (TOD::DrawTODSliderRow("Bloom Scale", hdrBloomScale, 0.0f, 10.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].hdrBloomScale = hdrBloomScale[i]; + changed = true; + } - // Cinematic Settings - if (ImGui::TreeNode("Cinematic Settings")) { - bool changed = false; - changed |= ImGui::SliderFloat("Saturation", &imgSpace.cinematicSaturation, 0.0f, 2.0f, "%.3f"); - changed |= ImGui::SliderFloat("Brightness", &imgSpace.cinematicBrightness, 0.0f, 2.0f, "%.3f"); - changed |= ImGui::SliderFloat("Contrast", &imgSpace.cinematicContrast, 0.0f, 2.0f, "%.3f"); + if (TOD::DrawTODSliderRow("Sunlight Scale", hdrSunlightScale, 0.0f, 10.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].hdrSunlightScale = hdrSunlightScale[i]; + changed = true; + } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - SetImageSpaceValues(); - } + if (TOD::DrawTODSliderRow("Sky Scale", hdrSkyScale, 0.0f, 10.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].hdrSkyScale = hdrSkyScale[i]; + changed = true; + } - ImGui::TreePop(); - } + // Separator + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); - // Tint Settings - if (ImGui::TreeNode("Tint Settings")) { - bool changed = false; - changed |= ImGui::ColorEdit3("Tint Color", &imgSpace.tintColor.x); - changed |= ImGui::SliderFloat("Tint Amount", &imgSpace.tintAmount, 0.0f, 1.0f, "%.3f"); + // Cinematic Settings + if (TOD::DrawTODSliderRow("Saturation", cinematicSaturation, 0.0f, 2.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].cinematicSaturation = cinematicSaturation[i]; + changed = true; + } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - SetImageSpaceValues(); - } + if (TOD::DrawTODSliderRow("Brightness", cinematicBrightness, 0.0f, 2.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].cinematicBrightness = cinematicBrightness[i]; + changed = true; + } - ImGui::TreePop(); - } + if (TOD::DrawTODSliderRow("Contrast", cinematicContrast, 0.0f, 2.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].cinematicContrast = cinematicContrast[i]; + changed = true; + } - // Depth of Field Settings - if (ImGui::TreeNode("Depth of Field")) { - bool changed = false; - changed |= ImGui::SliderFloat("DOF Strength", &imgSpace.dofStrength, 0.0f, 10.0f, "%.3f"); - changed |= ImGui::SliderFloat("DOF Distance", &imgSpace.dofDistance, 0.0f, 10000.0f, "%.1f"); - changed |= ImGui::SliderFloat("DOF Range", &imgSpace.dofRange, 0.0f, 10000.0f, "%.1f"); + // Separator + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - SetImageSpaceValues(); - } + // Tint Settings + if (TOD::DrawTODColorRow("Tint Color", tintColor)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].tintColor = tintColor[i]; + changed = true; + } - ImGui::TreePop(); - } + if (TOD::DrawTODSliderRow("Tint Amount", tintAmount, 0.0f, 1.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].tintAmount = tintAmount[i]; + changed = true; + } - ImGui::Spacing(); + // Separator + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + // Depth of Field + if (TOD::DrawTODSliderRow("DOF Strength", dofStrength, 0.0f, 10.0f, "%.3f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].dofStrength = dofStrength[i]; + changed = true; + } + + if (TOD::DrawTODSliderRow("DOF Distance", dofDistance, 0.0f, 10000.0f, "%.1f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].dofDistance = dofDistance[i]; + changed = true; } - ImGui::PopID(); + if (TOD::DrawTODSliderRow("DOF Range", dofRange, 0.0f, 10000.0f, "%.1f")) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.imageSpaces[i].dofRange = dofRange[i]; + changed = true; + } + + TOD::EndTODTable(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + SetImageSpaceValues(); } } @@ -1047,3 +1426,102 @@ void WeatherWidget::SetImageSpaceValues() data.depthOfField.range = imgSettings.dofRange; } } + +void WeatherWidget::UpdateSearchResults() +{ + searchResults.clear(); + + if (searchBuffer[0] == '\0') + return; + + std::string searchTerm = searchBuffer; + + // Search in Basic tab properties + std::vector>> basicCategories = { + { "Sun", { { "Sun Glare", 0 }, { "Sun Damage", 0 } } }, + { "Wind", { { "Wind Speed", 0 }, { "Wind Direction", 0 }, { "Wind Direction Range", 0 } } }, + { "Precipitation", { { "Precipitation Begin Fade In", 0 }, { "Precipitation Begin Fade Out", 0 } } }, + { "Lightning", { { "Thunder Lightning Begin Fade In", 0 }, { "Thunder Lightning End Fade Out", 0 }, + { "Thunder Lightning Frequency", 0 }, { "Lightning Color", 1 } } }, + { "Visual Effects", { { "Visual Effect Begin", 0 }, { "Visual Effect End", 0 } } }, + { "Weather Transition", { { "Trans Delta", 0 } } } + }; + + for (const auto& [category, properties] : basicCategories) { + for (const auto& [propName, type] : properties) { + if (ContainsStringIgnoreCase(propName, searchTerm)) { + searchResults.push_back({ propName, "Basic", propName }); + } + } + } + + // Search in Fog tab + std::vector fogProperties = { + "Day Near", "Day Far", "Day Power", "Day Max", + "Night Near", "Night Far", "Night Power", "Night Max" + }; + for (const auto& propName : fogProperties) { + if (ContainsStringIgnoreCase(propName, searchTerm)) { + searchResults.push_back({ propName, "Fog", propName }); + } + } + + // Search in DALC settings + std::vector dalcSettings = { + "Fresnel Power", "Specular", + "Directional X Max", "Directional X Min", + "Directional Y Max", "Directional Y Min", + "Directional Z Max", "Directional Z Min" + }; + for (const auto& setting : dalcSettings) { + if (ContainsStringIgnoreCase(setting, searchTerm)) { + searchResults.push_back({ setting, "Lighting (DALC)", setting }); + } + } + + // Search in Atmosphere Colors + for (int i = 0; i < ColorTypes::kTotal; i++) { + std::string colorType = ColorTypeLabel(i); + if (ContainsStringIgnoreCase(colorType, searchTerm)) { + searchResults.push_back({ colorType, "Atmosphere Colors", colorType }); + } + } + + // Search in Cloud settings + for (int i = 0; i < TESWeather::kTotalLayers; i++) { + std::string layer = std::format("Layer {}", i); + if (ContainsStringIgnoreCase(layer, searchTerm) || + ContainsStringIgnoreCase("Cloud", searchTerm)) { + searchResults.push_back({ std::format("Cloud {}", layer), "Clouds", layer }); + } + } + + // Search in ImageSpace settings + std::vector imageSpaceSettings = { + "Eye Adapt Speed", "Bloom Blur Radius", "Bloom Threshold", "Bloom Scale", + "Sunlight Scale", "Sky Scale", "Saturation", "Brightness", "Contrast", + "Tint Color", "Tint Amount", "DOF Strength", "DOF Distance", "DOF Range" + }; + for (const auto& setting : imageSpaceSettings) { + if (ContainsStringIgnoreCase(setting, searchTerm)) { + searchResults.push_back({ setting, "ImageSpace", setting }); + } + } +} + +void WeatherWidget::NavigateToSetting(const SearchResult& result) +{ + activeTabOverride = result.tabName; + highlightedSetting = result.settingId; + highlightStartTime = static_cast(ImGui::GetTime()); +} + +bool WeatherWidget::ShouldHighlight(const std::string& settingId) const +{ + if (highlightedSetting != settingId) + return false; + + float elapsed = static_cast(ImGui::GetTime()) - highlightStartTime; + return elapsed < 1.0f; // Highlight for 1 second +} + diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h index c0ece8f4cc..e1413ee86b 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.h +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -18,6 +18,8 @@ class WeatherWidget : public Widget form = a_weather; weather = a_weather; LoadWeatherValues(); + // Cache the original vanilla values for restoration + vanillaSettings = settings; } struct DirectionalColor @@ -91,6 +93,8 @@ class WeatherWidget : public Widget }; Settings settings; + // Cached original vanilla values for restoration + Settings vanillaSettings; ~WeatherWidget(); @@ -117,7 +121,22 @@ class WeatherWidget : public Widget void DrawDALCSettings(); void DrawWeatherColorSettings(); void DrawCloudSettings(); + void DrawFogSettings(); void DrawFeatureSettings(); + + // Search functionality + struct SearchResult { + std::string displayName; + std::string tabName; + std::string settingId; + }; + std::vector searchResults; + std::string activeTabOverride = ""; + std::string highlightedSetting = ""; + float highlightStartTime = 0.0f; + void UpdateSearchResults(); + void NavigateToSetting(const SearchResult& result); + bool ShouldHighlight(const std::string& settingId) const; void DrawImageSpaceSettings(); void DrawProperties(std::string category, std::map properties); void InheritFromParent(const std::string& property); diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp index d308c74662..d48f4c2cde 100644 --- a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp @@ -16,7 +16,7 @@ void WorldSpaceWidget::DrawWidget() if (!editorWindow->settings.autoApplyChanges) { auto menu = globals::menu; - bool useIcons = menu && menu->GetSettings().Theme.ShowActionIcons && + bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons && menu->uiIcons.saveSettings.texture && menu->uiIcons.featureSettingRevert.texture; @@ -47,17 +47,29 @@ void WorldSpaceWidget::DrawWidget() ImGui::PopStyleColor(2); ImGui::PopStyleVar(); } else { + const float buttonHeight = ImGui::GetFrameHeight(); + + ImVec2 applySize = ImGui::CalcTextSize("Apply"); + applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + applySize.y = buttonHeight; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", ImVec2(ImGui::GetContentRegionAvail().x * 0.49f, 0))) { + if (ImGui::Button("Apply", applySize)) { ApplyChanges(); } ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Apply changes to the game"); } + ImGui::SameLine(); + + ImVec2 revertSize = ImGui::CalcTextSize("Revert"); + revertSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + revertSize.y = buttonHeight; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - if (ImGui::Button("Revert", ImVec2(-1, 0))) { + if (ImGui::Button("Revert", revertSize)) { RevertChanges(); } ImGui::PopStyleColor(); @@ -67,6 +79,9 @@ void WorldSpaceWidget::DrawWidget() } } ImGui::Separator(); + + // Search bar (activatable with Ctrl+F) + BeginWidgetSearchBar(searchBuffer, sizeof(searchBuffer), searchActive); } ImGui::End(); } diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index d65195a222..d2112208ee 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -1,5 +1,16 @@ #include "WeatherUtils.h" +bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring) +{ + if (a_substring.empty()) + return true; + + const auto it = std::ranges::search(a_string, a_substring, [](const char a_a, const char a_b) { + return std::tolower(a_a) == std::tolower(a_b); + }); + return !it.empty(); +} + float Int8ToFloat(const int8_t& value) { return ((float)(value + 128) / 255.0f); @@ -149,4 +160,309 @@ bool DrawSliderUint8(const std::string& label, int& property) bool DrawSliderFloat(const std::string& label, float& property) { return ImGui::SliderFloat(label.c_str(), &property, 0, 50000); +} + +// Time of Day (TOD) helper implementation +namespace TOD +{ + const char* GetPeriodName(int index) + { + static const char* names[Count] = { "Sunrise", "Day", "Sunset", "Night" }; + if (index >= 0 && index < Count) + return names[index]; + return "Unknown"; + } + + float GetCurrentGameTime() + { + auto sky = globals::game::sky; + if (sky) { + return std::clamp(sky->currentGameHour, 0.0f, 24.0f); + } + return 12.0f; // Default to noon + } + + void GetTimeOfDayFactors(float outFactors[4]) + { + // Initialize all to 0 + for (int i = 0; i < 4; ++i) + outFactors[i] = 0.0f; + + float currentTime = GetCurrentGameTime(); + + // Simplified time periods (matching Skyrim's 4-period system) + // Sunrise: 5-9, Day: 9-17, Sunset: 17-21, Night: 21-5 + const float sunriseStart = 5.0f; + const float sunriseEnd = 9.0f; + const float dayStart = 9.0f; + const float dayEnd = 17.0f; + const float sunsetStart = 17.0f; + const float sunsetEnd = 21.0f; + + if (currentTime >= sunriseStart && currentTime < sunriseEnd) { + // Sunrise period + float t = (currentTime - sunriseStart) / (sunriseEnd - sunriseStart); + outFactors[Sunrise] = 1.0f - t; + outFactors[Day] = t; + } else if (currentTime >= dayStart && currentTime < dayEnd) { + // Day period + outFactors[Day] = 1.0f; + } else if (currentTime >= sunsetStart && currentTime < sunsetEnd) { + // Sunset period + float t = (currentTime - sunsetStart) / (sunsetEnd - sunsetStart); + outFactors[Day] = 1.0f - t; + outFactors[Sunset] = t; + } else if (currentTime >= sunsetEnd || currentTime < sunriseStart) { + // Night period + outFactors[Night] = 1.0f; + } + } + + int GetActivePeriod() + { + float factors[4]; + GetTimeOfDayFactors(factors); + + int maxIndex = 0; + float maxValue = factors[0]; + for (int i = 1; i < 4; ++i) { + if (factors[i] > maxValue) { + maxValue = factors[i]; + maxIndex = i; + } + } + return maxIndex; + } + + void RenderTODHeader() + { + float factors[4]; + GetTimeOfDayFactors(factors); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float sliderWidth = (totalWidth - 3 * spacing) / 4.0f; + + for (int i = 0; i < Count; ++i) { + if (i > 0) + ImGui::SameLine(); + + ImGui::BeginChild(("##todheader_" + std::to_string(i)).c_str(), + ImVec2(sliderWidth, ImGui::GetTextLineHeight()), false, ImGuiWindowFlags_NoScrollbar); + + const char* name = GetPeriodName(i); + float labelWidth = ImGui::CalcTextSize(name).x; + float centerOffset = (sliderWidth - labelWidth) * 0.5f; + if (centerOffset > 0) + ImGui::SetCursorPosX(centerOffset); + + bool isActive = factors[i] > 0.01f; + if (!isActive) + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.7f)); + + ImGui::Text("%s", name); + + if (!isActive) + ImGui::PopStyleColor(); + + ImGui::EndChild(); + } + } + + bool DrawTODSliderRow(const char* label, float values[4], float minValue, float maxValue, const char* format) + { + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float sliderWidth = (totalWidth - 3 * 8.0f) / 4.0f; + + for (int i = 0; i < Count; ++i) { + if (i > 0) + ImGui::SameLine(); + + bool isActive = factors[i] > 0.0f; + if (!isActive) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + + ImGui::PushItemWidth(sliderWidth); + std::string id = std::string("##") + label + std::to_string(i); + if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) + changed = true; + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); + + ImGui::PopItemWidth(); + + if (!isActive) + ImGui::PopStyleVar(); + } + + return changed; + } + + bool DrawTODColorRow(const char* label, float3 colors[4]) + { + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float spacing = ImGui::GetStyle().ItemSpacing.x; + // Match the header calculation exactly + float columnWidth = (totalWidth - 3 * spacing) / 4.0f; + + // Use a fixed button size + const float buttonSize = ImGui::GetFrameHeight() * 1.5f; + + for (int i = 0; i < Count; ++i) { + if (i > 0) + ImGui::SameLine(); + + bool isActive = factors[i] > 0.0f; + if (!isActive) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + + // Create a child region matching the column width to ensure proper alignment + ImGui::BeginChild(("##colorcolumn_" + std::string(label) + std::to_string(i)).c_str(), + ImVec2(columnWidth, buttonSize), false, ImGuiWindowFlags_NoScrollbar); + + // Center the button within this column + float centerOffset = (columnWidth - buttonSize) * 0.5f; + if (centerOffset > 0.0f) + ImGui::SetCursorPosX(centerOffset); + + std::string id = std::string("##") + label + std::to_string(i); + ImVec4 color = ImVec4(colors[i].x, colors[i].y, colors[i].z, 1.0f); + + // Use ColorButton with fixed size + if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { + ImGui::OpenPopup(id.c_str()); + } + + // Color picker popup + if (ImGui::BeginPopup(id.c_str())) { + if (ImGui::ColorPicker3((id + "_picker").c_str(), (float*)&colors[i], ImGuiColorEditFlags_NoAlpha)) { + changed = true; + } + ImGui::EndPopup(); + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); + + ImGui::EndChild(); + + if (!isActive) + ImGui::PopStyleVar(); + } + + return changed; + } + + bool DrawTODInt8Row(const char* label, int values[4]) + { + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float sliderWidth = (totalWidth - 3 * 8.0f) / 4.0f; + + for (int i = 0; i < Count; ++i) { + if (i > 0) + ImGui::SameLine(); + + bool isActive = factors[i] > 0.0f; + if (!isActive) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + + ImGui::PushItemWidth(sliderWidth); + std::string id = std::string("##") + label + std::to_string(i); + if (ImGui::SliderInt(id.c_str(), &values[i], -128, 127)) + changed = true; + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); + + ImGui::PopItemWidth(); + + if (!isActive) + ImGui::PopStyleVar(); + } + + return changed; + } + + bool BeginTODTable(const char* tableId) + { + if (ImGui::BeginTable(tableId, 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + return true; + } + return false; + } + + void EndTODTable() + { + ImGui::EndTable(); + } +} + +bool BeginWidgetSearchBar(char* searchBuffer, size_t bufferSize, bool& searchActive) +{ + // Check for Ctrl+F to activate search + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && + ImGui::IsKeyPressed(ImGuiKey_F, false) && ImGui::GetIO().KeyCtrl) { + searchActive = true; + } + + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.25f, 0.3f, 1.0f)); + ImGui::SetNextItemWidth(-100.0f); + + if (searchActive) { + ImGui::SetKeyboardFocusHere(); + searchActive = false; + } + + if (ImGui::InputTextWithHint("##WidgetSearch", "Search parameters... (Ctrl+F)", searchBuffer, bufferSize)) { + // Text changed + } + + // Clear button + ImGui::SameLine(); + if (ImGui::Button("Clear", ImVec2(90, 0))) { + searchBuffer[0] = '\0'; + } + + ImGui::PopStyleColor(); + ImGui::Separator(); + + return searchBuffer[0] != '\0'; // Return true if search is active +} + +void EndWidgetSearchBar() +{ + // Currently no cleanup needed, but keeping for symmetry and future use } \ No newline at end of file diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h index 3582a8310d..92c4db8c8f 100644 --- a/src/WeatherEditor/WeatherUtils.h +++ b/src/WeatherEditor/WeatherUtils.h @@ -1,6 +1,10 @@ #pragma once #include "Util.h" +#include + +// Case-insensitive substring search helper +bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring); void Float3ToColor(const float3& newColor, RE::Color& color); void Float3ToColor(const float3& newColor, RE::TESWeather::Data::Color3& color); @@ -22,4 +26,57 @@ enum ControlType COLOR3_PICKER, UINT8_SLIDER, FLOAT_SLIDER -}; \ No newline at end of file +}; + +// Time of Day (TOD) helper functions +namespace TOD +{ + // Time period indices + enum Period : int + { + Sunrise = 0, + Day = 1, + Sunset = 2, + Night = 3, + Count = 4 + }; + + // Get the name of a time period + const char* GetPeriodName(int index); + + // Get current game time in hours (0-24) + float GetCurrentGameTime(); + + // Calculate blend factor for each time period based on current game time + // Returns array of 4 floats (Sunrise, Day, Sunset, Night) + void GetTimeOfDayFactors(float outFactors[4]); + + // Get the primary active time period (highest blend factor) + int GetActivePeriod(); + + // Render TOD header row (shows period names with current activity) + void RenderTODHeader(); + + // Draw a horizontal row of TOD sliders + // Returns true if any slider changed + bool DrawTODSliderRow(const char* label, float values[4], float minValue = 0.0f, float maxValue = 1.0f, const char* format = "%.2f"); + + // Draw a horizontal row of TOD color pickers + // Returns true if any color changed + bool DrawTODColorRow(const char* label, float3 colors[4]); + + // Draw a horizontal row of TOD int8 sliders + // Returns true if any slider changed + bool DrawTODInt8Row(const char* label, int values[4]); + + // Helper to begin a TOD table (2 columns: Parameter | Values) + // Returns true if table was created successfully + bool BeginTODTable(const char* tableId); + + // End the TOD table + void EndTODTable(); +} // namespace TOD + +// Widget search bar helpers +bool BeginWidgetSearchBar(char* searchBuffer, size_t bufferSize, bool& searchActive); +void EndWidgetSearchBar(); \ No newline at end of file diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index e8ef3309fa..dc48ab9daa 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -2,6 +2,16 @@ #include "EditorWindow.h" #include "State.h" #include "Util.h" +#include "WeatherUtils.h" + +bool Widget::MatchesSearch(const std::string& text) const +{ + // If search is empty or inactive, match everything + if (searchBuffer[0] == '\0') { + return true; + } + return ContainsStringIgnoreCase(text, searchBuffer); +} void Widget::Save() { @@ -66,18 +76,28 @@ void Widget::Load() { std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName(), GetEditorID()); - std::ifstream settingsFile(filePath); - if (!std::filesystem::exists(filePath)) { - // No saved file exists, reload vanilla/default values - logger::info("{}: No settings file found, reloading vanilla values", GetEditorID()); + // No saved file exists, reset to vanilla/default values + logger::info("{}: No settings file found, resetting to vanilla values", GetEditorID()); js = json(); LoadSettings(); + + EditorWindow::GetSingleton()->ShowNotification( + std::format("No saved file - reset {} to vanilla values", GetEditorID()), + ImVec4(0.3f, 0.8f, 1.0f, 1.0f), + 3.0f); return; } + // File exists, load from it + std::ifstream settingsFile(filePath); + if (!settingsFile.good() || !settingsFile.is_open()) { - logger::warn("Failed to load settings file: {}", filePath); + logger::warn("Failed to open settings file: {}", filePath); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Failed to open file for {}", GetEditorID()), + ImVec4(1.0f, 0.5f, 0.0f, 1.0f), + 3.0f); return; } @@ -89,33 +109,44 @@ void Widget::Load() if (js.is_null()) { logger::warn("{}: Loaded JSON is null, file may be empty or invalid", filePath); EditorWindow::GetSingleton()->ShowNotification( - std::format("Failed to load settings for {}", GetEditorID()), - ImVec4(1.0f, 0.0f, 0.0f, 1.0f), + std::format("Invalid file for {} - resetting to vanilla", GetEditorID()), + ImVec4(1.0f, 0.5f, 0.0f, 1.0f), 3.0f); + js = json(); + LoadSettings(); return; } - logger::info("{}: Successfully loaded settings file", GetEditorID()); + logger::info("{}: Successfully loaded settings from file", GetEditorID()); + LoadSettings(); + EditorWindow::GetSingleton()->ShowNotification( + std::format("Loaded saved settings for {}", GetEditorID()), + ImVec4(0.0f, 1.0f, 0.5f, 1.0f), + 3.0f); + } catch (const nlohmann::json::parse_error& e) { logger::error("Error parsing settings for file ({}) : {}\n", filePath, e.what()); logger::error("Parse error at byte {}: {}", e.byte, e.what()); settingsFile.close(); EditorWindow::GetSingleton()->ShowNotification( - std::format("Failed to parse settings for {}", GetEditorID()), + std::format("Parse error for {} - resetting to vanilla", GetEditorID()), ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 3.0f); + js = json(); + LoadSettings(); return; } catch (const std::exception& e) { logger::error("Unexpected error loading settings file ({}) : {}\n", filePath, e.what()); settingsFile.close(); EditorWindow::GetSingleton()->ShowNotification( - std::format("Failed to load settings for {}", GetEditorID()), + std::format("Error loading {} - resetting to vanilla", GetEditorID()), ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 3.0f); + js = json(); + LoadSettings(); return; } - LoadSettings(); } void Widget::Delete() @@ -137,6 +168,9 @@ void Widget::Delete() // Reload settings from vanilla/mod defaults LoadSettings(); + // Apply the vanilla values to the game + ApplyChanges(); + EditorWindow::GetSingleton()->ShowNotification( std::format("Deleted {} - reverted to vanilla values", GetEditorID()), ImVec4(0.0f, 1.0f, 0.0f, 1.0f), @@ -146,6 +180,12 @@ void Widget::Delete() } } +bool Widget::HasSavedFile() const +{ + std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), const_cast(this)->GetFolderName(), GetEditorID()); + return std::filesystem::exists(filePath); +} + void Widget::DrawMenu() { if (ImGui::BeginMenuBar()) { diff --git a/src/WeatherEditor/Widget.h b/src/WeatherEditor/Widget.h index cb478471ea..418da43aa4 100644 --- a/src/WeatherEditor/Widget.h +++ b/src/WeatherEditor/Widget.h @@ -28,17 +28,17 @@ class Widget virtual ~Widget() {}; - virtual std::string GetEditorID() + virtual std::string GetEditorID() const { return form->GetFormEditorID(); } - virtual std::string GetFormID() + virtual std::string GetFormID() const { return std::format("{:08X}", form->GetFormID()); } - virtual std::string GetFilename() + virtual std::string GetFilename() const { if (auto file = form->GetFile()) return std::format("{}", file->GetFilename()); @@ -62,9 +62,18 @@ class Widget void Save(); void Load(); void Delete(); + bool HasSavedFile() const; virtual void LoadSettings() = 0; virtual void SaveSettings() = 0; + virtual void ApplyChanges() = 0; + virtual bool HasUnsavedChanges() const { return false; } + + // Search functionality + char searchBuffer[256] = ""; + bool searchActive = false; + + bool MatchesSearch(const std::string& text) const; protected: json js = json(); From 68ba2f23c76953a6aa55734606d8ad759c34505e Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 9 Dec 2025 00:03:22 +1000 Subject: [PATCH 06/30] DRY --- .../Weather/LightingTemplateWidget.cpp | 75 +---- src/WeatherEditor/Weather/WeatherWidget.cpp | 239 +-------------- .../Weather/WorldSpaceWidget.cpp | 75 +---- src/WeatherEditor/Widget.cpp | 271 ++++++++++++++++++ src/WeatherEditor/Widget.h | 4 + 5 files changed, 289 insertions(+), 375 deletions(-) diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index 0bac195792..a3efb7e0af 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -29,79 +29,10 @@ LightingTemplateWidget::~LightingTemplateWidget() void LightingTemplateWidget::DrawWidget() { ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); - if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) { - DrawMenu(); - - auto editorWindow = EditorWindow::GetSingleton(); - - if (!editorWindow->settings.autoApplyChanges) { - auto menu = globals::menu; - bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons && - menu->uiIcons.saveSettings.texture && - menu->uiIcons.featureSettingRevert.texture; - - if (useIcons) { - const float iconSize = ImGui::GetFrameHeight(); - const ImVec2 buttonSize(iconSize, iconSize); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); - - if (ImGui::ImageButton("##ApplyLightingTemplate", menu->uiIcons.saveSettings.texture, buttonSize)) { - ApplyChanges(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Apply changes to the game"); - } - - ImGui::SameLine(); - - if (ImGui::ImageButton("##RevertLightingTemplate", menu->uiIcons.featureSettingRevert.texture, buttonSize)) { - RevertChanges(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Revert to saved values"); - } - - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); - } else { - const float buttonHeight = ImGui::GetFrameHeight(); - - ImVec2 applySize = ImGui::CalcTextSize("Apply"); - applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - applySize.y = buttonHeight; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", applySize)) { - ApplyChanges(); - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Apply changes to the game"); - } - - ImGui::SameLine(); - - ImVec2 revertSize = ImGui::CalcTextSize("Revert"); - revertSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - revertSize.y = buttonHeight; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - if (ImGui::Button("Revert", revertSize)) { - RevertChanges(); - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Revert to saved values"); - } - } - } - ImGui::Separator(); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - // Search bar (activatable with Ctrl+F) - BeginWidgetSearchBar(searchBuffer, sizeof(searchBuffer), searchActive); + // Draw header with search and Save/Load/Delete buttons + DrawWidgetHeader("##LightingTemplateSearch", false, true, false, nullptr); if (ImGui::BeginTabBar("LightingTemplateSettingsTabs", ImGuiTabBarFlags_None)) { if (ImGui::BeginTabItem("Basic")) { diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 236618d793..c9d1e230b4 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -47,243 +47,19 @@ void WeatherWidget::DrawWidget() ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - auto editorWindow = EditorWindow::GetSingleton(); - bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; - - auto menu = globals::menu; - bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons && - menu->uiIcons.applyToGame.texture && - menu->uiIcons.saveSettings.texture && - menu->uiIcons.loadSettings.texture; - - if (useIcons) { - const float iconSize = ImGui::GetFrameHeight(); - const ImVec2 buttonSize(iconSize, iconSize); - - // Lock/Unlock text button - const char* lockLabel = isLocked ? "Unlock" : "Force Weather"; - ImVec2 lockTextSize = ImGui::CalcTextSize(lockLabel); - float lockButtonWidth = lockTextSize.x + ImGui::GetStyle().FramePadding.x * 2.0f; - - if (isLocked) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); - } - if (ImGui::Button(lockLabel, ImVec2(lockButtonWidth, iconSize))) { - if (isLocked) { - editorWindow->UnlockWeather(); - } else { - editorWindow->LockWeather(weather); - } - } - if (isLocked) { - ImGui::PopStyleColor(2); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(isLocked ? "Unlock Weather" : "Force This Weather"); - } - - ImGui::SameLine(); - - // Icon buttons with transparent background - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); - - // Apply to game button - only show when auto-apply is disabled - if (!editorWindow->settings.autoApplyChanges && menu->uiIcons.applyToGame.texture) { - if (ImGui::ImageButton("##ApplyToGame", menu->uiIcons.applyToGame.texture, buttonSize)) { - ApplyChanges(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Apply changes to the game"); - } - ImGui::SameLine(); - } - - // Save to file button - always visible - if (ImGui::ImageButton("##SaveWeather", menu->uiIcons.saveSettings.texture, buttonSize)) { - Save(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Save to file"); - } - - ImGui::SameLine(); - - // Load from file button - always visible - if (ImGui::ImageButton("##LoadWeather", menu->uiIcons.loadSettings.texture, buttonSize)) { - Load(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Load saved file (or reset to vanilla if no file)"); - } - - // Delete button - only visible if a saved file exists - if (HasSavedFile()) { - ImGui::SameLine(); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); - if (ImGui::ImageButton("##DeleteWeather", menu->uiIcons.deleteSettings.texture, buttonSize)) { - if (editorWindow->settings.suppressDeleteWarning) { - Delete(); - } else { - ImGui::OpenPopup("ConfirmDelete"); - } - } - ImGui::PopStyleColor(2); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Delete saved file and revert to defaults"); - } - } - - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); - } else { - // Text-based buttons - all inline with wrapping support - const float buttonHeight = ImGui::GetFrameHeight(); - const float spacing = ImGui::GetStyle().ItemSpacing.x; - - // Lock/Unlock button - const char* lockLabel = isLocked ? "Unlock" : "Lock"; - ImVec2 lockSize = ImGui::CalcTextSize(lockLabel); - lockSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - lockSize.y = buttonHeight; - - if (isLocked) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); - } - if (ImGui::Button(lockLabel, lockSize)) { - if (isLocked) { - editorWindow->UnlockWeather(); - } else { - editorWindow->LockWeather(weather); - } - } - if (isLocked) { - ImGui::PopStyleColor(2); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(isLocked ? "Unlock Weather" : "Lock & Force This Weather"); - } - - ImGui::SameLine(); - ImGui::Dummy(ImVec2(spacing * 2.0f, 0)); - ImGui::SameLine(); - - // Apply button - only show when auto-apply is disabled - if (!editorWindow->settings.autoApplyChanges) { - ImVec2 applySize = ImGui::CalcTextSize("Apply"); - applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - applySize.y = buttonHeight; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", applySize)) { - ApplyChanges(); - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Apply changes to the game"); - } - ImGui::SameLine(); - } - - // Save button - ImVec2 saveSize = ImGui::CalcTextSize("Save"); - saveSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - saveSize.y = buttonHeight; - - if (ImGui::Button("Save", saveSize)) { - Save(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Save to file"); - } - - ImGui::SameLine(); - - // Load button - ImVec2 loadSize = ImGui::CalcTextSize("Load"); - loadSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - loadSize.y = buttonHeight; - - if (ImGui::Button("Load", loadSize)) { - Load(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Load saved file (or reset to vanilla if no file)"); - } - - // Delete button - only visible if a saved file exists - if (HasSavedFile()) { - ImGui::SameLine(); - - ImVec2 deleteSize = ImGui::CalcTextSize("Delete"); - deleteSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - deleteSize.y = buttonHeight; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); - if (ImGui::Button("Delete", deleteSize)) { - if (editorWindow->settings.suppressDeleteWarning) { - Delete(); - } else { - ImGui::OpenPopup("ConfirmDelete"); - } - } - ImGui::PopStyleColor(2); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Delete saved file and revert to defaults"); - } - } - } - - // Confirmation popup for delete - if (ImGui::BeginPopupModal("ConfirmDelete", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Are you sure you want to delete this file?"); - ImGui::Text("This will revert to vanilla/mod provided values."); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - if (ImGui::Checkbox("Don't show this warning again", &editorWindow->settings.suppressDeleteWarning)) { - // Save the preference immediately - editorWindow->Save(); - } - - ImGui::Spacing(); - - if (ImGui::Button("Yes", ImVec2(120, 0))) { - Delete(); - ImGui::CloseCurrentPopup(); - } - ImGui::SetItemDefaultFocus(); - ImGui::SameLine(); - if (ImGui::Button("No", ImVec2(120, 0))) { - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } + // Draw header with search and all buttons + DrawWidgetHeader("##WeatherSearch", false, true, true, weather); - ImGui::Separator(); - - // Search bar with dropdown - ImGui::SetNextItemWidth(200.0f); - if (ImGui::InputTextWithHint("##WeatherSearch", "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer))) { + // Update search results when search buffer changes + if (searchActive) { UpdateSearchResults(); } - // Handle Ctrl+F to focus search - if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { - ImGui::SetKeyboardFocusHere(-1); - } - // Show search results dropdown if (searchBuffer[0] != '\0' && !searchResults.empty()) { - ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMax().y)); - ImGui::SetNextWindowSize(ImVec2(ImGui::GetItemRectSize().x * 1.5f, 0)); + // Find the search input position for dropdown placement + ImGui::SetNextWindowPos(ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y)); + ImGui::SetNextWindowSize(ImVec2(200.0f * 1.5f, 0)); ImGui::SetNextWindowFocus(); if (ImGui::Begin("##SearchDropdown", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | @@ -314,6 +90,7 @@ void WeatherWidget::DrawWidget() ImGui::End(); } + auto editorWindow = EditorWindow::GetSingleton(); auto& widgets = editorWindow->weatherWidgets; // Sets the parent widget if settings have been loaded. diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp index d48f4c2cde..a6367c4a4f 100644 --- a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp @@ -9,79 +9,10 @@ WorldSpaceWidget::~WorldSpaceWidget() void WorldSpaceWidget::DrawWidget() { ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); - if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar)) { - DrawMenu(); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - auto editorWindow = EditorWindow::GetSingleton(); - - if (!editorWindow->settings.autoApplyChanges) { - auto menu = globals::menu; - bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons && - menu->uiIcons.saveSettings.texture && - menu->uiIcons.featureSettingRevert.texture; - - if (useIcons) { - const float iconSize = ImGui::GetFrameHeight(); - const ImVec2 buttonSize(iconSize, iconSize); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); - - if (ImGui::ImageButton("##ApplyWorldSpace", menu->uiIcons.saveSettings.texture, buttonSize)) { - ApplyChanges(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Apply changes to the game"); - } - - ImGui::SameLine(); - - if (ImGui::ImageButton("##RevertWorldSpace", menu->uiIcons.featureSettingRevert.texture, buttonSize)) { - RevertChanges(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Revert to saved values"); - } - - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); - } else { - const float buttonHeight = ImGui::GetFrameHeight(); - - ImVec2 applySize = ImGui::CalcTextSize("Apply"); - applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - applySize.y = buttonHeight; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", applySize)) { - ApplyChanges(); - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Apply changes to the game"); - } - - ImGui::SameLine(); - - ImVec2 revertSize = ImGui::CalcTextSize("Revert"); - revertSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; - revertSize.y = buttonHeight; - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - if (ImGui::Button("Revert", revertSize)) { - RevertChanges(); - } - ImGui::PopStyleColor(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Revert to saved values"); - } - } - } - ImGui::Separator(); - - // Search bar (activatable with Ctrl+F) - BeginWidgetSearchBar(searchBuffer, sizeof(searchBuffer), searchActive); + // Draw header with search and Save/Load/Delete buttons + DrawWidgetHeader("##WorldSpaceSearch", false, true, false, nullptr); } ImGui::End(); } diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index dc48ab9daa..5da2be9b7b 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -254,3 +254,274 @@ std::string Widget::GetFolderName() return "Unknown"; } } + +void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool showSaveLoad, bool showForceWeather, RE::TESWeather* weather) +{ + auto editorWindow = EditorWindow::GetSingleton(); + auto menu = globals::menu; + bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons; + + if (useIcons) { + const float iconSize = ImGui::GetFrameHeight(); + const ImVec2 buttonSize(iconSize, iconSize); + + // Search bar first + ImGui::SetNextItemWidth(200.0f); + if (ImGui::InputTextWithHint(searchId, "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer))) { + searchActive = searchBuffer[0] != '\0'; + } + + // Handle Ctrl+F to focus search + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(-1); + } + + // Force Weather button (Weather widget only) + if (showForceWeather && weather) { + ImGui::SameLine(); + + bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; + const char* lockLabel = isLocked ? "Unlock" : "Force Weather"; + ImVec2 lockTextSize = ImGui::CalcTextSize(lockLabel); + float lockButtonWidth = lockTextSize.x + ImGui::GetStyle().FramePadding.x * 2.0f; + + if (isLocked) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); + } + if (ImGui::Button(lockLabel, ImVec2(lockButtonWidth, iconSize))) { + if (isLocked) { + editorWindow->UnlockWeather(); + } else { + editorWindow->LockWeather(weather); + } + } + if (isLocked) { + ImGui::PopStyleColor(2); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isLocked ? "Unlock Weather" : "Force This Weather"); + } + } + + // Icon buttons + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + + // Apply/Revert buttons + if (showApplyRevert && !editorWindow->settings.autoApplyChanges && menu->uiIcons.applyToGame.texture) { + ImGui::SameLine(); + if (ImGui::ImageButton((std::string(searchId) + "_Apply").c_str(), menu->uiIcons.applyToGame.texture, buttonSize)) { + ApplyChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + + if (menu->uiIcons.featureSettingRevert.texture) { + ImGui::SameLine(); + if (ImGui::ImageButton((std::string(searchId) + "_Revert").c_str(), menu->uiIcons.featureSettingRevert.texture, buttonSize)) { + RevertChanges(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } + } + } + + // Save/Load/Delete buttons + if (showSaveLoad && menu->uiIcons.saveSettings.texture && menu->uiIcons.loadSettings.texture) { + ImGui::SameLine(); + if (ImGui::ImageButton((std::string(searchId) + "_Save").c_str(), menu->uiIcons.saveSettings.texture, buttonSize)) { + Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save to file"); + } + + ImGui::SameLine(); + if (ImGui::ImageButton((std::string(searchId) + "_Load").c_str(), menu->uiIcons.loadSettings.texture, buttonSize)) { + Load(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load saved file (or reset to vanilla if no file)"); + } + + if (HasSavedFile() && menu->uiIcons.deleteSettings.texture) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::ImageButton((std::string(searchId) + "_Delete").c_str(), menu->uiIcons.deleteSettings.texture, buttonSize)) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + ImGui::OpenPopup("ConfirmDelete"); + } + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete saved file and revert to defaults"); + } + } + } + + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + } else { + // Text button mode + const float buttonHeight = ImGui::GetFrameHeight(); + + // Search bar first + ImGui::SetNextItemWidth(200.0f); + if (ImGui::InputTextWithHint(searchId, "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer))) { + searchActive = searchBuffer[0] != '\0'; + } + + // Handle Ctrl+F to focus search + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(-1); + } + + // Force Weather button (Weather widget only) + if (showForceWeather && weather) { + ImGui::SameLine(); + + bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; + const char* lockLabel = isLocked ? "Unlock" : "Force Weather"; + ImVec2 lockSize = ImGui::CalcTextSize(lockLabel); + lockSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + lockSize.y = buttonHeight; + + if (isLocked) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); + } + if (ImGui::Button(lockLabel, lockSize)) { + if (isLocked) { + editorWindow->UnlockWeather(); + } else { + editorWindow->LockWeather(weather); + } + } + if (isLocked) { + ImGui::PopStyleColor(2); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isLocked ? "Unlock Weather" : "Force This Weather"); + } + } + + // Apply/Revert buttons + if (showApplyRevert && !editorWindow->settings.autoApplyChanges) { + ImGui::SameLine(); + + ImVec2 applySize = ImGui::CalcTextSize("Apply"); + applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + applySize.y = buttonHeight; + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); + if (ImGui::Button("Apply", applySize)) { + ApplyChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Apply changes to the game"); + } + + ImGui::SameLine(); + + ImVec2 revertSize = ImGui::CalcTextSize("Revert"); + revertSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + revertSize.y = buttonHeight; + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + if (ImGui::Button("Revert", revertSize)) { + RevertChanges(); + } + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Revert to saved values"); + } + } + + // Save/Load/Delete buttons + if (showSaveLoad) { + ImGui::SameLine(); + + ImVec2 saveSize = ImGui::CalcTextSize("Save"); + saveSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + saveSize.y = buttonHeight; + + if (ImGui::Button("Save", saveSize)) { + Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Save to file"); + } + + ImGui::SameLine(); + + ImVec2 loadSize = ImGui::CalcTextSize("Load"); + loadSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + loadSize.y = buttonHeight; + + if (ImGui::Button("Load", loadSize)) { + Load(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Load saved file (or reset to vanilla if no file)"); + } + + if (HasSavedFile()) { + ImGui::SameLine(); + + ImVec2 deleteSize = ImGui::CalcTextSize("Delete"); + deleteSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; + deleteSize.y = buttonHeight; + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); + if (ImGui::Button("Delete", deleteSize)) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + ImGui::OpenPopup("ConfirmDelete"); + } + } + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Delete saved file and revert to defaults"); + } + } + } + } + + // Confirmation popup for delete (shared by all widgets) + if (showSaveLoad && ImGui::BeginPopupModal("ConfirmDelete", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Are you sure you want to delete this file?"); + ImGui::Text("This will revert to vanilla/mod provided values."); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Checkbox("Don't show this warning again", &editorWindow->settings.suppressDeleteWarning)) { + editorWindow->Save(); + } + + ImGui::Spacing(); + + if (ImGui::Button("Yes", ImVec2(120, 0))) { + Delete(); + ImGui::CloseCurrentPopup(); + } + ImGui::SetItemDefaultFocus(); + ImGui::SameLine(); + if (ImGui::Button("No", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::Separator(); +} diff --git a/src/WeatherEditor/Widget.h b/src/WeatherEditor/Widget.h index 418da43aa4..12c9d7bf74 100644 --- a/src/WeatherEditor/Widget.h +++ b/src/WeatherEditor/Widget.h @@ -67,8 +67,12 @@ class Widget virtual void LoadSettings() = 0; virtual void SaveSettings() = 0; virtual void ApplyChanges() = 0; + virtual void RevertChanges() { LoadSettings(); } virtual bool HasUnsavedChanges() const { return false; } + // Draw common header with search bar and action buttons + void DrawWidgetHeader(const char* searchId, bool showApplyRevert = true, bool showSaveLoad = false, bool showForceWeather = false, RE::TESWeather* weather = nullptr); + // Search functionality char searchBuffer[256] = ""; bool searchActive = false; From 014c33aecf721f5b6bb28d854204ce782f4a2370 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:19:11 +0000 Subject: [PATCH 07/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/WeatherEditor/EditorWindow.cpp | 433 +++++++++--------- src/WeatherEditor/EditorWindow.h | 11 +- .../Weather/LightingTemplateWidget.cpp | 99 ++-- src/WeatherEditor/Weather/WeatherWidget.cpp | 116 ++--- src/WeatherEditor/Weather/WeatherWidget.h | 5 +- .../Weather/WorldSpaceWidget.cpp | 1 - src/WeatherEditor/WeatherUtils.cpp | 22 +- src/WeatherEditor/Widget.cpp | 78 ++-- 8 files changed, 407 insertions(+), 358 deletions(-) diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 5297c19086..038333e095 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1,7 +1,7 @@ #include "EditorWindow.h" -#include "State.h" #include "Features/WeatherEditor.h" +#include "State.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" #include "imgui_internal.h" @@ -42,13 +42,13 @@ void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool filled) const int numPoints = 5; const float angleStep = 3.14159f / numPoints; ImVec2 points[10]; - + for (int i = 0; i < numPoints * 2; i++) { float angle = -1.57079f + i * angleStep; float r = (i % 2 == 0) ? radius : radius * 0.38f; points[i] = ImVec2(center.x + cosf(angle) * r, center.y + sinf(angle) * r); } - + if (filled) { // Draw filled star using triangles to avoid artifacts for (int i = 0; i < numPoints; i++) { @@ -82,9 +82,9 @@ void DrawIconWave(ImVec2 center, float width, ImU32 color, bool filled) const float amplitude = width * 0.15f; const float waveWidth = width * 0.8f; const float segmentWidth = waveWidth / segments; - + ImVec2 start(center.x - waveWidth * 0.5f, center.y); - + if (filled) { // Draw filled wave using multiple horizontal lines for (int i = 0; i < segments; i++) { @@ -110,23 +110,23 @@ bool IconButton(const char* label, bool filled, const char* iconType) { ImVec2 buttonSize(ImGui::GetFrameHeight(), ImGui::GetFrameHeight()); ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - + bool result = ImGui::InvisibleButton(label, buttonSize); - + bool hovered = ImGui::IsItemHovered(); bool active = ImGui::IsItemActive(); - - ImU32 bgColor = active ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : + + ImU32 bgColor = active ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : hovered ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) : - ImGui::GetColorU32(ImGuiCol_Button); + ImGui::GetColorU32(ImGuiCol_Button); ImU32 iconColor = ImGui::GetColorU32(ImGuiCol_Text); - + auto* drawList = ImGui::GetWindowDrawList(); drawList->AddRectFilled(cursorPos, ImVec2(cursorPos.x + buttonSize.x, cursorPos.y + buttonSize.y), bgColor, ImGui::GetStyle().FrameRounding); - + ImVec2 center(cursorPos.x + buttonSize.x * 0.5f, cursorPos.y + buttonSize.y * 0.5f); float iconSize = buttonSize.x * 0.35f; - + if (strcmp(iconType, "star") == 0) { DrawIconStar(center, iconSize, iconColor, filled); } else if (strcmp(iconType, "circle") == 0) { @@ -134,7 +134,7 @@ bool IconButton(const char* label, bool filled, const char* iconType) } else if (strcmp(iconType, "wave") == 0) { DrawIconWave(center, buttonSize.x * 0.7f, iconColor, filled); } - + return result; } @@ -190,16 +190,16 @@ void EditorWindow::ShowObjectsWindow() // Quick filter buttons on same row ImGui::SameLine(); - ImGui::Dummy(ImVec2(10.0f, 0.0f)); // Spacer + ImGui::Dummy(ImVec2(10.0f, 0.0f)); // Spacer ImGui::SameLine(); if (IconButton("##filterFavorites", showOnlyFavorites, "star")) { showOnlyFavorites = !showOnlyFavorites; } ImGui::SameLine(); ImGui::Text("Favorites"); - + ImGui::SameLine(); - ImGui::Dummy(ImVec2(10.0f, 0.0f)); // Spacer + ImGui::Dummy(ImVec2(10.0f, 0.0f)); // Spacer ImGui::SameLine(); if (IconButton("##filterFlagged", showOnlyFlagged, "circle")) { showOnlyFlagged = !showOnlyFlagged; @@ -213,7 +213,8 @@ void EditorWindow::ShowObjectsWindow() ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Recent:"); ImGui::SameLine(); for (size_t i = 0; i < std::min(size_t(5), settings.recentWidgets.size()); ++i) { - if (i > 0) ImGui::SameLine(); + if (i > 0) + ImGui::SameLine(); if (ImGui::SmallButton(settings.recentWidgets[i].c_str())) { // Find and open widget in all collections bool found = false; @@ -247,14 +248,14 @@ void EditorWindow::ShowObjectsWindow() // Create a table for the right column with "Name" and "ID" headers. Different weights to prevent truncation. if (ImGui::BeginTable("DetailsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_Sortable)) { - ImGui::TableSetupColumn("Fav", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoSort, 25.0f); // Favorite indicator - ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f); // Largest - weather/template names - ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 80.0f); // Fixed - 8 hex chars - ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Medium - plugin names - ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f); // Smaller - status text + ImGui::TableSetupColumn("Fav", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoSort, 25.0f); // Favorite indicator + ImGui::TableSetupColumn("Editor ID", ImGuiTableColumnFlags_WidthStretch, 3.5f); // Largest - weather/template names + ImGui::TableSetupColumn("Form ID", ImGuiTableColumnFlags_WidthFixed, 80.0f); // Fixed - 8 hex chars + ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Medium - plugin names + ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthStretch, 1.5f); // Smaller - status text ImGui::TableHeadersRow(); - + // Handle column sorting if (ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs()) { if (sortSpecs->SpecsDirty) { @@ -269,17 +270,17 @@ void EditorWindow::ShowObjectsWindow() } } - // Display objects based on the selected category - auto& widgets = selectedCategory == "Weather" ? weatherWidgets : - selectedCategory == "WorldSpace" ? worldSpaceWidgets : - lightingTemplateWidgets; - - // Sort widgets based on current sort column - std::vector sortedWidgets = widgets; - if (currentSortColumn != SortColumn::None) { - std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { - int comparison = 0; - switch (currentSortColumn) { + // Display objects based on the selected category + auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + lightingTemplateWidgets; + + // Sort widgets based on current sort column + std::vector sortedWidgets = widgets; + if (currentSortColumn != SortColumn::None) { + std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { + int comparison = 0; + switch (currentSortColumn) { case SortColumn::EditorID: comparison = _stricmp(a->GetEditorID().c_str(), b->GetEditorID().c_str()); break; @@ -289,37 +290,122 @@ void EditorWindow::ShowObjectsWindow() case SortColumn::File: comparison = _stricmp(a->GetFilename().c_str(), b->GetFilename().c_str()); break; - case SortColumn::Status: { - auto markerA = settings.markedRecords.find(a->GetEditorID()); - auto markerB = settings.markedRecords.find(b->GetEditorID()); - std::string statusA = (markerA != settings.markedRecords.end()) ? markerA->second : ""; - std::string statusB = (markerB != settings.markedRecords.end()) ? markerB->second : ""; - comparison = _stricmp(statusA.c_str(), statusB.c_str()); - break; - } + case SortColumn::Status: + { + auto markerA = settings.markedRecords.find(a->GetEditorID()); + auto markerB = settings.markedRecords.find(b->GetEditorID()); + std::string statusA = (markerA != settings.markedRecords.end()) ? markerA->second : ""; + std::string statusB = (markerB != settings.markedRecords.end()) ? markerB->second : ""; + comparison = _stricmp(statusA.c_str(), statusB.c_str()); + break; + } default: break; + } + return sortAscending ? (comparison < 0) : (comparison > 0); + }); + } + + // Get current cell's lighting template for prioritization + RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; + if (selectedCategory == "Lighting Template") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto& cellData = player->parentCell->GetRuntimeData(); + currentCellLightingTemplate = cellData.lightingTemplate; } - return sortAscending ? (comparison < 0) : (comparison > 0); - }); - } + } + + // Filtered display of widgets - show current cell's lighting template first + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + for (int i = 0; i < sortedWidgets.size(); ++i) { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) + continue; + + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + continue; + + // Apply quick filters + if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) + continue; + if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) + continue; + + auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); + auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); + ImGui::TableNextRow(); + + // Highlight current cell's lighting template + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - // Get current cell's lighting template for prioritization - RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; - if (selectedCategory == "Lighting Template") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto& cellData = player->parentCell->GetRuntimeData(); - currentCellLightingTemplate = cellData.lightingTemplate; + ImGui::TableSetColumnIndex(0); + + // Favorite star + if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + ToggleFavorite(sortedWidgets[i]->GetEditorID()); + } + + ImGui::TableNextColumn(); + + // Editor ID column with [CURRENT] prefix + bool isSelected = sortedWidgets[i]->IsOpen(); + if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); + } + } + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); + } + + // Context menu + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; + + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; + Save(); + } + } + + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(sortedWidgets[i]->GetEditorID()); + Save(); + } + + ImGui::EndPopup(); + } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); + + // File column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); + + // Status column + ImGui::TableNextColumn(); + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text("%s", markedRecord->second.c_str()); + } + } } - } - // Filtered display of widgets - show current cell's lighting template first - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + // Filtered display of widgets - regular list for (int i = 0; i < sortedWidgets.size(); ++i) { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) - continue; + // Skip current cell's lighting template if already shown + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) + continue; + } if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) continue; @@ -330,24 +416,27 @@ void EditorWindow::ShowObjectsWindow() if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) continue; - auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); - auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); + auto editorLabel = sortedWidgets[i]->GetEditorID(); + auto markedRecord = settings.markedRecords.find(editorLabel); ImGui::TableNextRow(); - // Highlight current cell's lighting template - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + // Set background colour + if (markedRecord != settings.markedRecords.end()) { + auto& color = settings.recordMarkers[markedRecord->second]; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); + } ImGui::TableSetColumnIndex(0); // Favorite star - if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { ToggleFavorite(sortedWidgets[i]->GetEditorID()); } ImGui::TableNextColumn(); - // Editor ID column with [CURRENT] prefix + // Editor ID column bool isSelected = sortedWidgets[i]->IsOpen(); if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { @@ -361,19 +450,19 @@ void EditorWindow::ShowObjectsWindow() AddToRecent(sortedWidgets[i]->GetEditorID()); } - // Context menu + // Opens a context menu on right click to mark records by color if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; for (auto& recordMarker : settings.recordMarkers) { if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; + settings.markedRecords[editorLabel] = recordMarker.first; Save(); } } if (ImGui::MenuItem("Remove")) { - markedRecords.erase(sortedWidgets[i]->GetEditorID()); + markedRecords.erase(editorLabel); Save(); } @@ -390,100 +479,14 @@ void EditorWindow::ShowObjectsWindow() // Status column ImGui::TableNextColumn(); + + // Re-check if the record exists after potential removal + markedRecord = settings.markedRecords.find(editorLabel); if (markedRecord != settings.markedRecords.end()) { ImGui::Text("%s", markedRecord->second.c_str()); } } - } - - // Filtered display of widgets - regular list - for (int i = 0; i < sortedWidgets.size(); ++i) { - // Skip current cell's lighting template if already shown - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) - continue; - } - - if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) - continue; - - // Apply quick filters - if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) - continue; - if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) - continue; - - auto editorLabel = sortedWidgets[i]->GetEditorID(); - auto markedRecord = settings.markedRecords.find(editorLabel); - ImGui::TableNextRow(); - - // Set background colour - if (markedRecord != settings.markedRecords.end()) { - auto& color = settings.recordMarkers[markedRecord->second]; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); - } - - ImGui::TableSetColumnIndex(0); - - // Favorite star - if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { - ToggleFavorite(sortedWidgets[i]->GetEditorID()); - } - - ImGui::TableNextColumn(); - - // Editor ID column - bool isSelected = sortedWidgets[i]->IsOpen(); - if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); - } - } - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); - } - - // Opens a context menu on right click to mark records by color - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { - auto& markedRecords = settings.markedRecords; - - for (auto& recordMarker : settings.recordMarkers) { - if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[editorLabel] = recordMarker.first; - Save(); - } - } - - if (ImGui::MenuItem("Remove")) { - markedRecords.erase(editorLabel); - Save(); - } - - ImGui::EndPopup(); - } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); - - // File column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); - - // Status column - ImGui::TableNextColumn(); - - // Re-check if the record exists after potential removal - markedRecord = settings.markedRecords.find(editorLabel); - if (markedRecord != settings.markedRecords.end()) { - ImGui::Text("%s", markedRecord->second.c_str()); - } - } ImGui::EndTable(); + ImGui::EndTable(); } ImGui::EndTable(); @@ -606,11 +609,11 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Save All Open Widgets", "Ctrl+S")) { SaveAll(); } - + // Save individual widgets submenu if (ImGui::BeginMenu("Save")) { bool hasOpenWidgets = false; - + // Weather widgets for (auto* widget : weatherWidgets) { if (widget->IsOpen()) { @@ -620,7 +623,7 @@ void EditorWindow::RenderUI() } } } - + // WorldSpace widgets for (auto* widget : worldSpaceWidgets) { if (widget->IsOpen()) { @@ -630,7 +633,7 @@ void EditorWindow::RenderUI() } } } - + // Lighting Template widgets for (auto* widget : lightingTemplateWidgets) { if (widget->IsOpen()) { @@ -640,7 +643,7 @@ void EditorWindow::RenderUI() } } } - + // ImageSpace widgets for (auto* widget : imageSpaceWidgets) { if (widget->IsOpen()) { @@ -650,14 +653,14 @@ void EditorWindow::RenderUI() } } } - + if (!hasOpenWidgets) { ImGui::TextDisabled("No open widgets"); } - + ImGui::EndMenu(); } - + ImGui::Separator(); if (ImGui::MenuItem("Close All Weather Widgets")) { for (auto* widget : weatherWidgets) widget->SetOpen(false); @@ -695,7 +698,7 @@ void EditorWindow::RenderUI() if (ImGui::BeginMenu("Window")) { ImGui::Text("Open Widgets:"); ImGui::Separator(); - + int openCount = 0; for (auto* widget : weatherWidgets) { if (widget->IsOpen()) { @@ -729,11 +732,11 @@ void EditorWindow::RenderUI() } } } - + if (openCount == 0) { ImGui::TextDisabled("No widgets open"); } - + ImGui::EndMenu(); } if (ImGui::BeginMenu("Help")) { @@ -764,12 +767,12 @@ void EditorWindow::RenderUI() ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "Recent: %d", (int)settings.recentWidgets.size()); ImGui::EndMenu(); } - + // Pause Time button auto menu = globals::menu; if (menu && menu->uiIcons.pauseTime.texture) { bool isPaused = IsTimePaused(); - + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); if (isPaused) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.1f, 1.0f)); @@ -778,11 +781,11 @@ void EditorWindow::RenderUI() ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); } - + const float menuBarHeight = ImGui::GetFrameHeight(); const float buttonDim = menuBarHeight * 0.85f; // 85% of menu bar height const ImVec2 buttonSize(buttonDim, buttonDim); - + if (ImGui::ImageButton("##GlobalPauseTime", menu->uiIcons.pauseTime.texture, buttonSize)) { if (isPaused) { ResumeTime(); @@ -790,15 +793,15 @@ void EditorWindow::RenderUI() PauseTime(); } } - + ImGui::PopStyleColor(2); ImGui::PopStyleVar(); - + if (ImGui::IsItemHovered()) { ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); } } - + // Weather lock indicator if (weatherLockActive && lockedWeather) { ImGui::SameLine(); @@ -807,7 +810,7 @@ void EditorWindow::RenderUI() ImGui::Text(" [LOCKED: %s]", weatherName ? weatherName : "Unknown"); ImGui::PopStyleColor(); } - + // Time pause indicator if (timePaused) { ImGui::SameLine(); @@ -815,7 +818,7 @@ void EditorWindow::RenderUI() ImGui::Text(" [TIME PAUSED]"); ImGui::PopStyleColor(); } - + // Close button on the right side float menuBarHeight = ImGui::GetFrameHeight(); ImGui::SameLine(ImGui::GetWindowWidth() - menuBarHeight - 10.0f); @@ -829,7 +832,7 @@ void EditorWindow::RenderUI() if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Close Weather Editor (Esc)"); } - + ImGui::EndMainMenuBar(); } @@ -902,7 +905,7 @@ void EditorWindow::Draw() { // Track editor open state for vanity camera management static bool wasOpen = false; - + if (open && !wasOpen) { // Editor just opened - disable vanity camera and restore session DisableVanityCamera(); @@ -912,7 +915,7 @@ void EditorWindow::Draw() RestoreVanityCamera(); SaveSessionWidgets(); } - + wasOpen = open; // Re-enforce weather lock if active (handles time changes) @@ -1014,23 +1017,23 @@ void EditorWindow::ShowSettingsWindow() if (selectedOption == "General") { ImGui::Checkbox("Auto-apply changes", &settings.autoApplyChanges); AddTooltip("Automatically apply changes to weather/lighting when editing"); - + ImGui::Checkbox("Suppress delete warnings", &settings.suppressDeleteWarning); AddTooltip("Don't show confirmation dialog when deleting saved files"); - + ImGui::Checkbox("Use text buttons instead of icons", &settings.useTextButtons); AddTooltip("Display action buttons as text labels instead of icons"); - + ImGui::Separator(); ImGui::TextUnformatted("Session & History"); ImGui::Spacing(); - + ImGui::Checkbox("Remember open widgets", &settings.rememberOpenWidgets); AddTooltip("Automatically reopen widgets that were open when you last closed the editor"); - + ImGui::SliderInt("Max recent widgets", &settings.maxRecentWidgets, 5, 20); AddTooltip("Maximum number of recent widgets to remember"); - + if (ImGui::Button("Clear Recent History")) { settings.recentWidgets.clear(); Save(); @@ -1040,7 +1043,7 @@ void EditorWindow::ShowSettingsWindow() settings.favoriteWidgets.clear(); Save(); } - + } else if (selectedOption == "Flags") { if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); @@ -1048,29 +1051,29 @@ void EditorWindow::ShowSettingsWindow() ImGui::TableSetupColumn("Actions", ImGuiTableColumnFlags_WidthFixed, 60.0f); auto& recordMarkers = settings.recordMarkers; - + // Store markers to delete (can't delete while iterating) static std::string markerToDelete; markerToDelete.clear(); - + // Store rename info (old name -> new name) static std::pair renameInfo; static bool needsRename = false; - + // Store separate buffers for each marker static std::unordered_map> labelBuffers; for (auto& recordMarker : recordMarkers) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - + // Editable label - use separate buffer for each marker auto& labelBuffer = labelBuffers[recordMarker.first]; if (labelBuffer[0] == '\0' || labelBuffers.find(recordMarker.first) == labelBuffers.end()) { strncpy_s(labelBuffer.data(), labelBuffer.size(), recordMarker.first.c_str(), labelBuffer.size() - 1); labelBuffer[labelBuffer.size() - 1] = '\0'; } - + ImGui::SetNextItemWidth(-1); if (ImGui::InputText(std::format("##Label{}", recordMarker.first).c_str(), labelBuffer.data(), labelBuffer.size(), ImGuiInputTextFlags_EnterReturnsTrue)) { // Mark for rename only on Enter @@ -1082,7 +1085,7 @@ void EditorWindow::ShowSettingsWindow() if (ImGui::ColorEdit3(std::format("Color##{}", recordMarker.first).c_str(), (float*)&recordMarker.second)) { Save(); } - + ImGui::TableSetColumnIndex(2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { @@ -1090,7 +1093,7 @@ void EditorWindow::ShowSettingsWindow() } ImGui::PopStyleColor(); } - + // Process rename if (needsRename && renameInfo.first != renameInfo.second && !renameInfo.second.empty()) { // Check if new name doesn't already exist @@ -1098,23 +1101,23 @@ void EditorWindow::ShowSettingsWindow() auto color = recordMarkers[renameInfo.first]; recordMarkers.erase(renameInfo.first); recordMarkers[renameInfo.second] = color; - + // Update any records that were using the old marker name for (auto& [recordId, markerName] : settings.markedRecords) { if (markerName == renameInfo.first) { markerName = renameInfo.second; } } - + Save(); } needsRename = false; } - + // Process deletion if (!markerToDelete.empty()) { recordMarkers.erase(markerToDelete); - + // Remove any records that were using this marker for (auto it = settings.markedRecords.begin(); it != settings.markedRecords.end();) { if (it->second == markerToDelete) { @@ -1123,7 +1126,7 @@ void EditorWindow::ShowSettingsWindow() ++it; } } - + Save(); } @@ -1203,10 +1206,12 @@ void EditorWindow::Load() void EditorWindow::LockWeather(RE::TESWeather* weather) { - if (!weather) return; + if (!weather) + return; auto sky = RE::Sky::GetSingleton(); - if (!sky) return; + if (!sky) + return; // Force the weather to be active sky->ForceWeather(weather, false); @@ -1219,7 +1224,8 @@ void EditorWindow::LockWeather(RE::TESWeather* weather) void EditorWindow::UnlockWeather() { - if (!weatherLockActive) return; + if (!weatherLockActive) + return; auto sky = RE::Sky::GetSingleton(); if (sky) { @@ -1235,7 +1241,8 @@ void EditorWindow::UnlockWeather() void EditorWindow::PauseTime() { - if (timePaused) return; + if (timePaused) + return; auto calendar = RE::Calendar::GetSingleton(); if (calendar && calendar->timeScale) { @@ -1248,7 +1255,8 @@ void EditorWindow::PauseTime() void EditorWindow::ResumeTime() { - if (!timePaused) return; + if (!timePaused) + return; auto calendar = RE::Calendar::GetSingleton(); if (calendar && calendar->timeScale) { @@ -1260,7 +1268,8 @@ void EditorWindow::ResumeTime() void EditorWindow::DisableVanityCamera() { - if (vanityCameraDisabled) return; + if (vanityCameraDisabled) + return; auto setting = RE::GetINISetting("fAutoVanityModeDelay:Camera"); if (setting) { @@ -1273,7 +1282,8 @@ void EditorWindow::DisableVanityCamera() void EditorWindow::RestoreVanityCamera() { - if (!vanityCameraDisabled) return; + if (!vanityCameraDisabled) + return; auto setting = RE::GetINISetting("fAutoVanityModeDelay:Camera"); if (setting) { @@ -1307,7 +1317,7 @@ void EditorWindow::RenderNotifications() // Render active notifications for (auto& notif : notifications) { float elapsed = currentTime - notif.startTime; - float fadeStart = notif.duration - 0.5f; // Start fading 0.5s before end + float fadeStart = notif.duration - 0.5f; // Start fading 0.5s before end float alpha = 1.0f; // Fade out in the last 0.5 seconds @@ -1325,7 +1335,6 @@ void EditorWindow::RenderNotifications() if (ImGui::Begin(std::format("##Notification{}", (void*)¬if).c_str(), nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { - ImVec4 colorWithAlpha = notif.color; colorWithAlpha.w *= alpha; ImGui::PushStyleColor(ImGuiCol_Text, colorWithAlpha); @@ -1378,7 +1387,7 @@ bool EditorWindow::IsFavorite(const std::string& widgetId) const void EditorWindow::SaveSessionWidgets() { settings.lastOpenWidgets.clear(); - + // Save all currently open widgets for (auto widget : weatherWidgets) { if (widget->IsOpen()) { @@ -1395,7 +1404,7 @@ void EditorWindow::SaveSessionWidgets() settings.lastOpenWidgets.push_back(widget->GetEditorID()); } } - + SaveSettings(); } diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index f735193e71..fbaf2e983e 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -113,9 +113,16 @@ class EditorWindow json j; std::string settingsFilename = "EditorSettings"; bool showSettingsWindow = false; - + // Sorting state - enum class SortColumn { None, EditorID, FormID, File, Status }; + enum class SortColumn + { + None, + EditorID, + FormID, + File, + Status + }; SortColumn currentSortColumn = SortColumn::None; bool sortAscending = true; }; \ No newline at end of file diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index a3efb7e0af..f2aa5e093f 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -30,7 +30,6 @@ void LightingTemplateWidget::DrawWidget() { ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - // Draw header with search and Save/Load/Delete buttons DrawWidgetHeader("##LightingTemplateSearch", false, true, false, nullptr); @@ -62,34 +61,50 @@ void LightingTemplateWidget::DrawBasicSettings() if (ImGui::CollapsingHeader("Ambient & Directional", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Ambient Color") && DrawColorEdit("Ambient Color", settings.ambient)) changed = true; - if (MatchesSearch("Ambient Color")) ImGui::Spacing(); - if (MatchesSearch("Directional Color") && DrawColorEdit("Directional Color", settings.directional)) changed = true; - if (MatchesSearch("Directional Color")) ImGui::Spacing(); + if (MatchesSearch("Ambient Color") && DrawColorEdit("Ambient Color", settings.ambient)) + changed = true; + if (MatchesSearch("Ambient Color")) + ImGui::Spacing(); + if (MatchesSearch("Directional Color") && DrawColorEdit("Directional Color", settings.directional)) + changed = true; + if (MatchesSearch("Directional Color")) + ImGui::Spacing(); } if (ImGui::CollapsingHeader("Directional Settings", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Directional XY") && DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; - if (MatchesSearch("Directional XY")) ImGui::Spacing(); - if (MatchesSearch("Directional Z") && DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; - if (MatchesSearch("Directional Z")) ImGui::Spacing(); - if (MatchesSearch("Directional Fade") && DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; - if (MatchesSearch("Directional Fade")) ImGui::Spacing(); + if (MatchesSearch("Directional XY") && DrawSliderFloat("Directional XY", settings.directionalXY)) + changed = true; + if (MatchesSearch("Directional XY")) + ImGui::Spacing(); + if (MatchesSearch("Directional Z") && DrawSliderFloat("Directional Z", settings.directionalZ)) + changed = true; + if (MatchesSearch("Directional Z")) + ImGui::Spacing(); + if (MatchesSearch("Directional Fade") && DrawSliderFloat("Directional Fade", settings.directionalFade)) + changed = true; + if (MatchesSearch("Directional Fade")) + ImGui::Spacing(); } if (ImGui::CollapsingHeader("Light Fade", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Light Fade Start") && DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; - if (MatchesSearch("Light Fade Start")) ImGui::Spacing(); - if (MatchesSearch("Light Fade End") && DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; - if (MatchesSearch("Light Fade End")) ImGui::Spacing(); + if (MatchesSearch("Light Fade Start") && DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) + changed = true; + if (MatchesSearch("Light Fade Start")) + ImGui::Spacing(); + if (MatchesSearch("Light Fade End") && DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) + changed = true; + if (MatchesSearch("Light Fade End")) + ImGui::Spacing(); } if (ImGui::CollapsingHeader("Other", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Clip Distance") && DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; - if (MatchesSearch("Clip Distance")) ImGui::Spacing(); + if (MatchesSearch("Clip Distance") && DrawSliderFloat("Clip Distance", settings.clipDist)) + changed = true; + if (MatchesSearch("Clip Distance")) + ImGui::Spacing(); } if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { @@ -102,22 +117,34 @@ void LightingTemplateWidget::DrawFogSettings() bool changed = false; ImGui::Spacing(); - if (MatchesSearch("Fog Color Near") && DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; - if (MatchesSearch("Fog Color Near")) ImGui::Spacing(); - if (MatchesSearch("Fog Color Far") && DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; - if (MatchesSearch("Fog Color Far")) ImGui::Spacing(); + if (MatchesSearch("Fog Color Near") && DrawColorEdit("Fog Color Near", settings.fogColorNear)) + changed = true; + if (MatchesSearch("Fog Color Near")) + ImGui::Spacing(); + if (MatchesSearch("Fog Color Far") && DrawColorEdit("Fog Color Far", settings.fogColorFar)) + changed = true; + if (MatchesSearch("Fog Color Far")) + ImGui::Spacing(); ImGui::Spacing(); - if (MatchesSearch("Fog Near") && DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; - if (MatchesSearch("Fog Near")) ImGui::Spacing(); - if (MatchesSearch("Fog Far") && DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; - if (MatchesSearch("Fog Far")) ImGui::Spacing(); + if (MatchesSearch("Fog Near") && DrawSliderFloat("Fog Near", settings.fogNear)) + changed = true; + if (MatchesSearch("Fog Near")) + ImGui::Spacing(); + if (MatchesSearch("Fog Far") && DrawSliderFloat("Fog Far", settings.fogFar)) + changed = true; + if (MatchesSearch("Fog Far")) + ImGui::Spacing(); ImGui::Spacing(); - if (MatchesSearch("Fog Power") && DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; - if (MatchesSearch("Fog Power")) ImGui::Spacing(); - if (MatchesSearch("Fog Clamp") && DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; - if (MatchesSearch("Fog Clamp")) ImGui::Spacing(); + if (MatchesSearch("Fog Power") && DrawSliderFloat("Fog Power", settings.fogPower)) + changed = true; + if (MatchesSearch("Fog Power")) + ImGui::Spacing(); + if (MatchesSearch("Fog Clamp") && DrawSliderFloat("Fog Clamp", settings.fogClamp)) + changed = true; + if (MatchesSearch("Fog Clamp")) + ImGui::Spacing(); if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); @@ -130,16 +157,18 @@ void LightingTemplateWidget::DrawDALCSettings() if (ImGui::CollapsingHeader("Basic DALC", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawColorEdit("Specular", settings.dalc.specular)) changed = true; + if (DrawColorEdit("Specular", settings.dalc.specular)) + changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; + if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) + changed = true; ImGui::Spacing(); } if (ImGui::CollapsingHeader("Directional Colors (Time of Day)", ImGuiTreeNodeFlags_DefaultOpen)) { // Note: The game engine uses X, Y, Z to represent time periods // We map them to: X=Sunrise/Day, Y=Sunset, Z=Night for TOD display - + if (TOD::BeginTODTable("DALCDirectionalTable")) { TOD::RenderTODHeader(); ImGui::TableNextRow(); @@ -151,17 +180,17 @@ void LightingTemplateWidget::DrawDALCSettings() // Prepare arrays for TOD rendering (map X,Y,Z to Sunrise,Day,Sunset,Night) float3 maxColors[4]; float3 minColors[4]; - + // Map X (index 0) to Sunrise and Day maxColors[TOD::Sunrise] = settings.dalc.directional[0].max; maxColors[TOD::Day] = settings.dalc.directional[0].max; minColors[TOD::Sunrise] = settings.dalc.directional[0].min; minColors[TOD::Day] = settings.dalc.directional[0].min; - + // Map Y (index 1) to Sunset maxColors[TOD::Sunset] = settings.dalc.directional[1].max; minColors[TOD::Sunset] = settings.dalc.directional[1].min; - + // Map Z (index 2) to Night maxColors[TOD::Night] = settings.dalc.directional[2].max; minColors[TOD::Night] = settings.dalc.directional[2].min; diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index c9d1e230b4..83e5496503 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -46,41 +46,39 @@ void WeatherWidget::DrawWidget() { ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - // Draw header with search and all buttons DrawWidgetHeader("##WeatherSearch", false, true, true, weather); - + // Update search results when search buffer changes if (searchActive) { UpdateSearchResults(); } - + // Show search results dropdown if (searchBuffer[0] != '\0' && !searchResults.empty()) { // Find the search input position for dropdown placement ImGui::SetNextWindowPos(ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y)); ImGui::SetNextWindowSize(ImVec2(200.0f * 1.5f, 0)); ImGui::SetNextWindowFocus(); - if (ImGui::Begin("##SearchDropdown", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { - + if (ImGui::Begin("##SearchDropdown", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { for (size_t i = 0; i < std::min(size_t(5), searchResults.size()); ++i) { const auto& result = searchResults[i]; std::string label = std::format("{} ({})", result.displayName, result.tabName); - + if (ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_DontClosePopups)) { NavigateToSetting(result); searchBuffer[0] = '\0'; searchResults.clear(); } } - + if (searchResults.size() > 5) { ImGui::Separator(); ImGui::TextDisabled("... %zu more results", searchResults.size() - 5); } - + // Close dropdown if clicking outside or pressing Escape if (!ImGui::IsWindowFocused() || ImGui::IsKeyPressed(ImGuiKey_Escape)) { searchBuffer[0] = '\0'; @@ -145,9 +143,9 @@ void WeatherWidget::DrawWidget() ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; ImGuiTabItemFlags imageSpaceFlags = (activeTabOverride == "ImageSpace") ? ImGuiTabItemFlags_SetSelected : 0; if (!activeTabOverride.empty()) { - activeTabOverride = ""; // Clear after use + activeTabOverride = ""; // Clear after use } - + if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { DrawProperties("Sun", { { "Sun Glare", INT8_SLIDER }, { "Sun Damage", INT8_SLIDER } }); DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); @@ -198,12 +196,12 @@ void WeatherWidget::DrawWidget() void WeatherWidget::LoadSettings() { bool hadErrors = false; - + if (!js.empty()) { try { // Attempt to load settings from JSON settings = js; - + // Validate that critical fields were loaded correctly if (js.contains("weatherProperties") && settings.weatherProperties.empty() && !js["weatherProperties"].empty()) { logger::warn("Weather {}: weatherProperties loaded but appears empty, reverting to vanilla values", GetEditorID()); @@ -217,7 +215,7 @@ void WeatherWidget::LoadSettings() logger::warn("Weather {}: fogProperties loaded but appears empty, reverting to vanilla values", GetEditorID()); hadErrors = true; } - + if (hadErrors) { // Fallback to vanilla/game values LoadWeatherValues(); @@ -232,7 +230,7 @@ void WeatherWidget::LoadSettings() settings.weatherColors.size(), settings.fogProperties.size()); } - + } catch (const nlohmann::json::exception& e) { logger::error("Weather {}: Failed to deserialize settings from JSON: {}", GetEditorID(), e.what()); logger::error("JSON content: {}", js.dump(2)); @@ -255,17 +253,17 @@ void WeatherWidget::LoadSettings() void WeatherWidget::SaveSettings() { SaveFeatureSettings(); - + try { js = settings; - + // Log what we're saving for debugging logger::info("Weather {}: Saving settings - {} weather properties, {} colors, {} fog properties", GetEditorID(), settings.weatherProperties.size(), settings.weatherColors.size(), settings.fogProperties.size()); - + // Validate serialization worked if (js.is_null()) { logger::error("Weather {}: Serialization produced null JSON!", GetEditorID()); @@ -276,7 +274,7 @@ void WeatherWidget::SaveSettings() } else if (!js.contains("clouds")) { logger::error("Weather {}: Serialized JSON missing clouds field!", GetEditorID()); } - + } catch (const nlohmann::json::exception& e) { logger::error("Weather {}: Failed to serialize settings to JSON: {}", GetEditorID(), e.what()); } @@ -636,9 +634,11 @@ void WeatherWidget::DrawCloudSettings() std::string layer = std::format("Layer {}", i); if (ImGui::CollapsingHeader(layer.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; + if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) + changed = true; ImGui::Spacing(); - if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; + if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) + changed = true; ImGui::Spacing(); if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { @@ -682,7 +682,7 @@ void WeatherWidget::DrawFogSettings() ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthFixed, 250.0f); ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthFixed, 250.0f); - + // Header row ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -690,7 +690,7 @@ void WeatherWidget::DrawFogSettings() ImGui::Text("Day"); ImGui::TableSetColumnIndex(2); ImGui::Text("Night"); - + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Separator(); @@ -763,7 +763,7 @@ void WeatherWidget::DrawFogSettings() void WeatherWidget::DrawProperties(std::string category, std::map properties) { bool& doesInherit = settings.inheritance[category]; - + // Check if any property matches search (only check if search is active) bool hasMatchingProperty = false; if (searchBuffer[0] != '\0') { @@ -781,7 +781,7 @@ void WeatherWidget::DrawProperties(std::string category, std::map(ImGui::GetTime()) - highlightStartTime; - float alpha = 0.3f * (1.0f - std::abs(elapsed - 0.5f) * 2.0f); // Fade in/out over 1 second + float alpha = 0.3f * (1.0f - std::abs(elapsed - 0.5f) * 2.0f); // Fade in/out over 1 second ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.6f, 1.0f, alpha)); ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.7f, 1.0f, alpha)); } - + switch (p.second) { case 0: - if (DrawSliderInt8(p.first, settings.weatherProperties[p.first])) changed = true; + if (DrawSliderInt8(p.first, settings.weatherProperties[p.first])) + changed = true; break; case 1: - if (DrawColorEdit(p.first, settings.weatherColors[p.first])) changed = true; + if (DrawColorEdit(p.first, settings.weatherColors[p.first])) + changed = true; break; case 2: - if (DrawSliderUint8(p.first, settings.weatherProperties[p.first])) changed = true; + if (DrawSliderUint8(p.first, settings.weatherProperties[p.first])) + changed = true; break; case 3: - if (DrawSliderFloat(p.first, settings.fogProperties[p.first])) changed = true; + if (DrawSliderFloat(p.first, settings.fogProperties[p.first])) + changed = true; break; default: break; } - + if (ShouldHighlight(p.first)) { ImGui::PopStyleColor(2); } @@ -850,7 +854,7 @@ void WeatherWidget::InheritFromParent(const std::string& property) void WeatherWidget::SaveFeatureSettings() { auto* weatherManager = WeatherManager::GetSingleton(); - + for (const auto& [featureName, featureJson] : settings.featureSettings) { if (!featureJson.empty()) { weatherManager->SaveSettingsToWeather(weather, featureName, featureJson); @@ -862,14 +866,14 @@ void WeatherWidget::LoadFeatureSettings() { auto* weatherManager = WeatherManager::GetSingleton(); auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); - + for (auto* feature : Feature::GetFeatureList()) { if (!feature || !feature->loaded) { continue; } std::string featureName = feature->GetShortName(); - + // Check if feature has registered weather variables if (!globalRegistry->HasWeatherSupport(featureName)) { continue; @@ -907,7 +911,7 @@ void WeatherWidget::DrawFeatureSettings() } std::string featureName = feature->GetShortName(); - + // Check if feature has registered weather variables if (!globalRegistry->HasWeatherSupport(featureName)) { continue; @@ -925,7 +929,7 @@ void WeatherWidget::DrawFeatureSettings() if (hasSettings) { ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); - + if (ImGui::Button("Clear Settings")) { settings.featureSettings[featureName] = json::object(); } @@ -946,8 +950,9 @@ void WeatherWidget::DrawFeatureSettings() } ImGui::Spacing(); - ImGui::TextWrapped("Note: Feature settings should be configured through the feature's own settings panel. " - "This section shows which features have per-weather overrides."); + ImGui::TextWrapped( + "Note: Feature settings should be configured through the feature's own settings panel. " + "This section shows which features have per-weather overrides."); ImGui::TreePop(); } @@ -1207,23 +1212,23 @@ void WeatherWidget::SetImageSpaceValues() void WeatherWidget::UpdateSearchResults() { searchResults.clear(); - + if (searchBuffer[0] == '\0') return; - + std::string searchTerm = searchBuffer; - + // Search in Basic tab properties std::vector>> basicCategories = { { "Sun", { { "Sun Glare", 0 }, { "Sun Damage", 0 } } }, { "Wind", { { "Wind Speed", 0 }, { "Wind Direction", 0 }, { "Wind Direction Range", 0 } } }, { "Precipitation", { { "Precipitation Begin Fade In", 0 }, { "Precipitation Begin Fade Out", 0 } } }, - { "Lightning", { { "Thunder Lightning Begin Fade In", 0 }, { "Thunder Lightning End Fade Out", 0 }, - { "Thunder Lightning Frequency", 0 }, { "Lightning Color", 1 } } }, + { "Lightning", { { "Thunder Lightning Begin Fade In", 0 }, { "Thunder Lightning End Fade Out", 0 }, + { "Thunder Lightning Frequency", 0 }, { "Lightning Color", 1 } } }, { "Visual Effects", { { "Visual Effect Begin", 0 }, { "Visual Effect End", 0 } } }, { "Weather Transition", { { "Trans Delta", 0 } } } }; - + for (const auto& [category, properties] : basicCategories) { for (const auto& [propName, type] : properties) { if (ContainsStringIgnoreCase(propName, searchTerm)) { @@ -1231,7 +1236,7 @@ void WeatherWidget::UpdateSearchResults() } } } - + // Search in Fog tab std::vector fogProperties = { "Day Near", "Day Far", "Day Power", "Day Max", @@ -1242,7 +1247,7 @@ void WeatherWidget::UpdateSearchResults() searchResults.push_back({ propName, "Fog", propName }); } } - + // Search in DALC settings std::vector dalcSettings = { "Fresnel Power", "Specular", @@ -1255,7 +1260,7 @@ void WeatherWidget::UpdateSearchResults() searchResults.push_back({ setting, "Lighting (DALC)", setting }); } } - + // Search in Atmosphere Colors for (int i = 0; i < ColorTypes::kTotal; i++) { std::string colorType = ColorTypeLabel(i); @@ -1263,16 +1268,16 @@ void WeatherWidget::UpdateSearchResults() searchResults.push_back({ colorType, "Atmosphere Colors", colorType }); } } - + // Search in Cloud settings for (int i = 0; i < TESWeather::kTotalLayers; i++) { std::string layer = std::format("Layer {}", i); - if (ContainsStringIgnoreCase(layer, searchTerm) || + if (ContainsStringIgnoreCase(layer, searchTerm) || ContainsStringIgnoreCase("Cloud", searchTerm)) { searchResults.push_back({ std::format("Cloud {}", layer), "Clouds", layer }); } } - + // Search in ImageSpace settings std::vector imageSpaceSettings = { "Eye Adapt Speed", "Bloom Blur Radius", "Bloom Threshold", "Bloom Scale", @@ -1297,8 +1302,7 @@ bool WeatherWidget::ShouldHighlight(const std::string& settingId) const { if (highlightedSetting != settingId) return false; - + float elapsed = static_cast(ImGui::GetTime()) - highlightStartTime; - return elapsed < 1.0f; // Highlight for 1 second + return elapsed < 1.0f; // Highlight for 1 second } - diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h index e1413ee86b..273241843d 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.h +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -123,9 +123,10 @@ class WeatherWidget : public Widget void DrawCloudSettings(); void DrawFogSettings(); void DrawFeatureSettings(); - + // Search functionality - struct SearchResult { + struct SearchResult + { std::string displayName; std::string tabName; std::string settingId; diff --git a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp index a6367c4a4f..3089e7cd30 100644 --- a/src/WeatherEditor/Weather/WorldSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/WorldSpaceWidget.cpp @@ -10,7 +10,6 @@ void WorldSpaceWidget::DrawWidget() { ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - // Draw header with search and Save/Load/Delete buttons DrawWidgetHeader("##WorldSpaceSearch", false, true, false, nullptr); } diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index d2112208ee..7b2257ffa8 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -4,7 +4,7 @@ bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string { if (a_substring.empty()) return true; - + const auto it = std::ranges::search(a_string, a_substring, [](const char a_a, const char a_b) { return std::tolower(a_a) == std::tolower(a_b); }); @@ -250,7 +250,7 @@ namespace TOD if (i > 0) ImGui::SameLine(); - ImGui::BeginChild(("##todheader_" + std::to_string(i)).c_str(), + ImGui::BeginChild(("##todheader_" + std::to_string(i)).c_str(), ImVec2(sliderWidth, ImGui::GetTextLineHeight()), false, ImGuiWindowFlags_NoScrollbar); const char* name = GetPeriodName(i); @@ -326,7 +326,7 @@ namespace TOD float spacing = ImGui::GetStyle().ItemSpacing.x; // Match the header calculation exactly float columnWidth = (totalWidth - 3 * spacing) / 4.0f; - + // Use a fixed button size const float buttonSize = ImGui::GetFrameHeight() * 1.5f; @@ -349,12 +349,12 @@ namespace TOD std::string id = std::string("##") + label + std::to_string(i); ImVec4 color = ImVec4(colors[i].x, colors[i].y, colors[i].z, 1.0f); - + // Use ColorButton with fixed size if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { ImGui::OpenPopup(id.c_str()); } - + // Color picker popup if (ImGui::BeginPopup(id.c_str())) { if (ImGui::ColorPicker3((id + "_picker").c_str(), (float*)&colors[i], ImGuiColorEditFlags_NoAlpha)) { @@ -434,31 +434,31 @@ bool BeginWidgetSearchBar(char* searchBuffer, size_t bufferSize, bool& searchAct { // Check for Ctrl+F to activate search if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && - ImGui::IsKeyPressed(ImGuiKey_F, false) && ImGui::GetIO().KeyCtrl) { + ImGui::IsKeyPressed(ImGuiKey_F, false) && ImGui::GetIO().KeyCtrl) { searchActive = true; } ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.25f, 0.3f, 1.0f)); ImGui::SetNextItemWidth(-100.0f); - + if (searchActive) { ImGui::SetKeyboardFocusHere(); searchActive = false; } - + if (ImGui::InputTextWithHint("##WidgetSearch", "Search parameters... (Ctrl+F)", searchBuffer, bufferSize)) { // Text changed } - + // Clear button ImGui::SameLine(); if (ImGui::Button("Clear", ImVec2(90, 0))) { searchBuffer[0] = '\0'; } - + ImGui::PopStyleColor(); ImGui::Separator(); - + return searchBuffer[0] != '\0'; // Return true if search is active } diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index 5da2be9b7b..0338349a57 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -49,7 +49,7 @@ void Widget::Save() } logger::info("{}: Saving settings file: {}", GetEditorID(), file); - + // Write with indentation for readability settingsFile << js.dump(2); settingsFile.flush(); @@ -62,7 +62,7 @@ void Widget::Save() settingsFile.close(); logger::info("{}: Successfully saved settings", GetEditorID()); - + } catch (const nlohmann::json::exception& e) { logger::error("{}: JSON error while saving settings: {}", GetEditorID(), e.what()); settingsFile.close(); @@ -81,7 +81,7 @@ void Widget::Load() logger::info("{}: No settings file found, resetting to vanilla values", GetEditorID()); js = json(); LoadSettings(); - + EditorWindow::GetSingleton()->ShowNotification( std::format("No saved file - reset {} to vanilla values", GetEditorID()), ImVec4(0.3f, 0.8f, 1.0f, 1.0f), @@ -104,7 +104,7 @@ void Widget::Load() try { settingsFile >> js; settingsFile.close(); - + // Validate that we loaded valid JSON if (js.is_null()) { logger::warn("{}: Loaded JSON is null, file may be empty or invalid", filePath); @@ -116,15 +116,15 @@ void Widget::Load() LoadSettings(); return; } - + logger::info("{}: Successfully loaded settings from file", GetEditorID()); LoadSettings(); - + EditorWindow::GetSingleton()->ShowNotification( std::format("Loaded saved settings for {}", GetEditorID()), ImVec4(0.0f, 1.0f, 0.5f, 1.0f), 3.0f); - + } catch (const nlohmann::json::parse_error& e) { logger::error("Error parsing settings for file ({}) : {}\n", filePath, e.what()); logger::error("Parse error at byte {}: {}", e.byte, e.what()); @@ -161,16 +161,16 @@ void Widget::Delete() try { std::filesystem::remove(filePath); logger::info("Deleted settings file: {}", filePath); - + // Clear the in-memory JSON data js = json(); - + // Reload settings from vanilla/mod defaults LoadSettings(); - + // Apply the vanilla values to the game ApplyChanges(); - + EditorWindow::GetSingleton()->ShowNotification( std::format("Deleted {} - reverted to vanilla values", GetEditorID()), ImVec4(0.0f, 1.0f, 0.0f, 1.0f), @@ -206,7 +206,7 @@ void Widget::DrawMenu() ImGui::OpenPopup("ConfirmDelete"); } } - + // Confirmation popup for delete if (ImGui::BeginPopupModal("ConfirmDelete", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Are you sure you want to delete this file?"); @@ -214,15 +214,15 @@ void Widget::DrawMenu() ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - + auto& settings = EditorWindow::GetSingleton()->settings; if (ImGui::Checkbox("Don't show this warning again", &settings.suppressDeleteWarning)) { // Save the preference immediately EditorWindow::GetSingleton()->Save(); } - + ImGui::Spacing(); - + if (ImGui::Button("Yes", ImVec2(120, 0))) { Delete(); ImGui::CloseCurrentPopup(); @@ -234,7 +234,7 @@ void Widget::DrawMenu() } ImGui::EndPopup(); } - + ImGui::EndMenu(); } ImGui::EndMenuBar(); @@ -270,7 +270,7 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s if (ImGui::InputTextWithHint(searchId, "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer))) { searchActive = searchBuffer[0] != '\0'; } - + // Handle Ctrl+F to focus search if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { ImGui::SetKeyboardFocusHere(-1); @@ -279,12 +279,12 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s // Force Weather button (Weather widget only) if (showForceWeather && weather) { ImGui::SameLine(); - + bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; const char* lockLabel = isLocked ? "Unlock" : "Force Weather"; ImVec2 lockTextSize = ImGui::CalcTextSize(lockLabel); float lockButtonWidth = lockTextSize.x + ImGui::GetStyle().FramePadding.x * 2.0f; - + if (isLocked) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); @@ -377,7 +377,7 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s if (ImGui::InputTextWithHint(searchId, "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer))) { searchActive = searchBuffer[0] != '\0'; } - + // Handle Ctrl+F to focus search if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { ImGui::SetKeyboardFocusHere(-1); @@ -386,13 +386,13 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s // Force Weather button (Weather widget only) if (showForceWeather && weather) { ImGui::SameLine(); - + bool isLocked = editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather; const char* lockLabel = isLocked ? "Unlock" : "Force Weather"; ImVec2 lockSize = ImGui::CalcTextSize(lockLabel); lockSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; lockSize.y = buttonHeight; - + if (isLocked) { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.3f, 1.0f)); @@ -415,11 +415,11 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s // Apply/Revert buttons if (showApplyRevert && !editorWindow->settings.autoApplyChanges) { ImGui::SameLine(); - + ImVec2 applySize = ImGui::CalcTextSize("Apply"); applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; applySize.y = buttonHeight; - + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); if (ImGui::Button("Apply", applySize)) { ApplyChanges(); @@ -428,13 +428,13 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Apply changes to the game"); } - + ImGui::SameLine(); - + ImVec2 revertSize = ImGui::CalcTextSize("Revert"); revertSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; revertSize.y = buttonHeight; - + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); if (ImGui::Button("Revert", revertSize)) { RevertChanges(); @@ -448,38 +448,38 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s // Save/Load/Delete buttons if (showSaveLoad) { ImGui::SameLine(); - + ImVec2 saveSize = ImGui::CalcTextSize("Save"); saveSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; saveSize.y = buttonHeight; - + if (ImGui::Button("Save", saveSize)) { Save(); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Save to file"); } - + ImGui::SameLine(); - + ImVec2 loadSize = ImGui::CalcTextSize("Load"); loadSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; loadSize.y = buttonHeight; - + if (ImGui::Button("Load", loadSize)) { Load(); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Load saved file (or reset to vanilla if no file)"); } - + if (HasSavedFile()) { ImGui::SameLine(); - + ImVec2 deleteSize = ImGui::CalcTextSize("Delete"); deleteSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; deleteSize.y = buttonHeight; - + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); if (ImGui::Button("Delete", deleteSize)) { @@ -504,13 +504,13 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - + if (ImGui::Checkbox("Don't show this warning again", &editorWindow->settings.suppressDeleteWarning)) { editorWindow->Save(); } - + ImGui::Spacing(); - + if (ImGui::Button("Yes", ImVec2(120, 0))) { Delete(); ImGui::CloseCurrentPopup(); @@ -522,6 +522,6 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s } ImGui::EndPopup(); } - + ImGui::Separator(); } From 5a092b90b2832bcb147c6ac248ca5a7f191d8a47 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Tue, 9 Dec 2025 23:44:07 +1000 Subject: [PATCH 08/30] updates --- .../WeatherVariableRegistration.md | 10 +- .../Shaders/WeatherEditor/DiffuseIBLCS.hlsl | 45 - src/Deferred.cpp | 18 +- src/Features/WeatherEditor.cpp | 111 +- src/Features/WeatherEditor.h | 11 +- src/Utils/Form.cpp | 3 + src/Utils/Serialize.cpp | 16 +- src/Utils/UI.cpp | 85 +- src/Utils/UI.h | 6 + src/WeatherEditor/EditorWindow.cpp | 635 ++++++-- src/WeatherEditor/EditorWindow.h | 63 +- src/WeatherEditor/PaletteWindow.cpp | 495 ++++++ src/WeatherEditor/PaletteWindow.h | 60 + .../Weather/CellLightingWidget.cpp | 404 +++++ .../Weather/CellLightingWidget.h | 70 + .../Weather/ImageSpaceWidget.cpp | 125 +- src/WeatherEditor/Weather/ImageSpaceWidget.h | 4 + src/WeatherEditor/Weather/LensFlareWidget.cpp | 70 + src/WeatherEditor/Weather/LensFlareWidget.h | 34 + .../Weather/LightingTemplateWidget.cpp | 39 +- .../Weather/LightingTemplateWidget.h | 4 + .../Weather/PrecipitationWidget.cpp | 186 +++ .../Weather/PrecipitationWidget.h | 45 + .../Weather/ReferenceEffectWidget.cpp | 123 ++ .../Weather/ReferenceEffectWidget.h | 40 + src/WeatherEditor/Weather/SimpleFormWidget.h | 40 + .../Weather/VolumetricLightingWidget.cpp | 156 ++ .../Weather/VolumetricLightingWidget.h | 44 + src/WeatherEditor/Weather/WeatherWidget.cpp | 1349 ++++++++++------- src/WeatherEditor/Weather/WeatherWidget.h | 21 +- src/WeatherEditor/WeatherUtils.cpp | 572 ++++++- src/WeatherEditor/WeatherUtils.h | 152 +- src/WeatherEditor/Widget.cpp | 33 +- src/WeatherEditor/Widget.h | 73 +- src/WeatherVariableRegistry.h | 30 +- 35 files changed, 4080 insertions(+), 1092 deletions(-) delete mode 100644 features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl create mode 100644 src/WeatherEditor/PaletteWindow.cpp create mode 100644 src/WeatherEditor/PaletteWindow.h create mode 100644 src/WeatherEditor/Weather/CellLightingWidget.cpp create mode 100644 src/WeatherEditor/Weather/CellLightingWidget.h create mode 100644 src/WeatherEditor/Weather/LensFlareWidget.cpp create mode 100644 src/WeatherEditor/Weather/LensFlareWidget.h create mode 100644 src/WeatherEditor/Weather/PrecipitationWidget.cpp create mode 100644 src/WeatherEditor/Weather/PrecipitationWidget.h create mode 100644 src/WeatherEditor/Weather/ReferenceEffectWidget.cpp create mode 100644 src/WeatherEditor/Weather/ReferenceEffectWidget.h create mode 100644 src/WeatherEditor/Weather/SimpleFormWidget.h create mode 100644 src/WeatherEditor/Weather/VolumetricLightingWidget.cpp create mode 100644 src/WeatherEditor/Weather/VolumetricLightingWidget.h diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md index f4aab35fbb..f496e69294 100644 --- a/docs/weather-system-docs/WeatherVariableRegistration.md +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -242,15 +242,7 @@ Uses nlohmann::json for type conversion. Built-in support for: - Missing JSON keys use default values - Type mismatches caught by json exceptions -- Invalid weather files logged but don't crashherVariables::FloatVariable>( - "intensity", "Intensity", "Effect intensity", - &settings.intensity, 1.0f, 0.0f, 2.0f - )); - } - - // That's it! No save/load/update code needed for weather variables - - }; +- Invalid weather files are logged but do not crash the system ``` diff --git a/features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl b/features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl deleted file mode 100644 index 82de8d246c..0000000000 --- a/features/Weather Editor/Shaders/WeatherEditor/DiffuseIBLCS.hlsl +++ /dev/null @@ -1,45 +0,0 @@ -#include "Common/Color.hlsli" -#include "Common/Math.hlsli" -#include "Common/Spherical Harmonics/SphericalHarmonics.hlsli" - -TextureCube ReflectionTexture : register(t0); -RWTexture2D IBLTexture : register(u0); - -SamplerState LinearSampler : register(s0); - -[numthreads(1, 1, 1)] void main(uint3 dispatchID - : SV_DispatchThreadID) { - // Initialise sh to 0 - sh2 shR = SphericalHarmonics::Zero(); - sh2 shG = SphericalHarmonics::Zero(); - sh2 shB = SphericalHarmonics::Zero(); - - float axisSampleCount = 32; - - // Accumulate coefficients according to surounding direction/color tuples. - for (float az = 0.5; az < axisSampleCount; az += 1.0) - for (float ze = 0.5; ze < axisSampleCount; ze += 1.0) { - float3 rayDir = SphericalHarmonics::GetUniformSphereSample(az / axisSampleCount, ze / axisSampleCount); - - float3 color = ReflectionTexture.SampleLevel(LinearSampler, -rayDir, 0); - - color = Color::GammaToLinear(color); - - sh2 sh = SphericalHarmonics::Evaluate(rayDir); - - shR = SphericalHarmonics::Add(shR, SphericalHarmonics::Scale(sh, color.r)); - shG = SphericalHarmonics::Add(shG, SphericalHarmonics::Scale(sh, color.g)); - shB = SphericalHarmonics::Add(shB, SphericalHarmonics::Scale(sh, color.b)); - } - - // Integrating over a sphere so each sample has a weight of 4*PI/samplecount (uniform solid angle, for each sample) - float shFactor = 4.0 * Math::PI / (axisSampleCount * axisSampleCount); - - shR = SphericalHarmonics::Scale(shR, shFactor); - shG = SphericalHarmonics::Scale(shG, shFactor); - shB = SphericalHarmonics::Scale(shB, shFactor); - - IBLTexture[int2(0, 0)] = shR; - IBLTexture[int2(1, 0)] = shG; - IBLTexture[int2(2, 0)] = shB; -} diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 0c7fe1394b..267e1a12c1 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -299,8 +299,6 @@ void Deferred::PrepassPasses() void Deferred::StartDeferred() { - WeatherEditor::GetSingleton()->Bind(); - if (!globals::state->inWorld) return; globals::state->UpdateSharedData(true, false); @@ -418,16 +416,14 @@ void Deferred::DeferredPasses() dynamicCubemaps.UpdateCubemap(); auto& terrainBlending = globals::features::terrainBlending; - auto& weatherEditor = globals::features::weatherEditor; auto& ibl = globals::features::ibl; - auto shadowMask = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kSHADOW_MASK]; // Deferred Composite { TracyD3D11Zone(globals::state->tracyCtx, "Deferred Composite"); - ID3D11ShaderResourceView* srvs[17]{ + ID3D11ShaderResourceView* srvs[16]{ specular.SRV, albedo.SRV, normalRoughness.SRV, @@ -442,15 +438,12 @@ void Deferred::DeferredPasses() ssgi_hq_spec ? nullptr : ssgi_y, ssgi_hq_spec ? nullptr : ssgi_cocg, ssgi_hq_spec ? ssgi_gi_spec : nullptr, - weatherEditor.loaded ? weatherEditor.diffuseIBLTexture->srv.get() : nullptr, ibl.loaded ? ibl.diffuseIBLTexture->srv.get() : nullptr, ibl.loaded ? ibl.diffuseSkyIBLTexture->srv.get() : nullptr, }; - ID3D11SamplerState* samplers[]{ - dynamicCubemaps.loaded ? linearSampler : nullptr, - }; - context->CSSetSamplers(0, ARRAYSIZE(samplers), samplers); + if (dynamicCubemaps.loaded) + context->CSSetSamplers(0, 1, &linearSampler); context->CSSetShaderResources(0, ARRAYSIZE(srvs), srvs); @@ -465,7 +458,7 @@ void Deferred::DeferredPasses() // Clear { - ID3D11ShaderResourceView* views[17]{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; + ID3D11ShaderResourceView* views[16]{ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; context->CSSetShaderResources(0, ARRAYSIZE(views), views); ID3D11UnorderedAccessView* uavs[3]{ nullptr, nullptr, nullptr }; @@ -474,9 +467,6 @@ void Deferred::DeferredPasses() ID3D11Buffer* buffers[1] = { nullptr }; context->CSSetConstantBuffers(12, 1, buffers); - ID3D11SamplerState* samplers[2]{ nullptr, nullptr }; - context->CSSetSamplers(0, ARRAYSIZE(samplers), samplers); - context->CSSetShader(nullptr, nullptr, 0); } diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp index a64f91f182..875ea1bc16 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/WeatherEditor.cpp @@ -21,7 +21,7 @@ uint8_t LerpUint8_t(const uint8_t oldValue, const uint8_t newVal, const float le void LerpColor(const RE::TESWeather::Data::Color3& oldColor, RE::TESWeather::Data::Color3& newColor, const float changePct) { - newColor.red = (int8_t)std::lerp(-128, 127, changePct); + newColor.red = LerpInt8_t(oldColor.red, newColor.red, changePct); newColor.green = (int8_t)std::lerp(oldColor.green, newColor.green, changePct); newColor.blue = LerpInt8_t(oldColor.blue, newColor.blue, changePct); } @@ -56,115 +56,6 @@ void WeatherEditor::DrawSettings() DrawWeatherStatusPanel(); } -void WeatherEditor::Bind() -{ - if (loaded) { - auto& context = globals::d3d::context; - - // Set PS shader resource - { - ID3D11ShaderResourceView* srv = diffuseIBLTexture->srv.get(); - context->PSSetShaderResources(80, 1, &srv); - } - } -} - -void WeatherEditor::Prepass() -{ - auto& context = globals::d3d::context; - - auto renderer = RE::BSGraphics::Renderer::GetSingleton(); - auto& reflections = renderer->GetRendererData().cubemapRenderTargets[RE::RENDER_TARGET_CUBEMAP::kREFLECTIONS]; - - std::array srvs = { reflections.SRV }; - std::array uavs = { diffuseIBLTexture->uav.get() }; - std::array samplers = { Deferred::GetSingleton()->linearSampler }; - - // Unset PS shader resource - { - ID3D11ShaderResourceView* srv = nullptr; - context->PSSetShaderResources(80, 1, &srv); - } - - // IBL - { - samplers[0] = Deferred::GetSingleton()->linearSampler; - - context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); - context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); - context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); - context->CSSetShader(GetDiffuseIBLCS(), nullptr, 0); - context->Dispatch(1, 1, 1); - } - - // Reset - { - srvs.fill(nullptr); - uavs.fill(nullptr); - samplers.fill(nullptr); - - context->CSSetSamplers(0, (uint)samplers.size(), samplers.data()); - context->CSSetShaderResources(0, (uint)srvs.size(), srvs.data()); - context->CSSetUnorderedAccessViews(0, (uint)uavs.size(), uavs.data(), nullptr); - context->CSSetShader(nullptr, nullptr, 0); - } - - // Set PS shader resource - { - ID3D11ShaderResourceView* srv = diffuseIBLTexture->srv.get(); - context->PSSetShaderResources(100, 1, &srv); - } -} - -void WeatherEditor::SetupResources() -{ - GetDiffuseIBLCS(); - - { - D3D11_TEXTURE2D_DESC texDesc{ - .Width = 3, - .Height = 1, - .MipLevels = 1, - .ArraySize = 1, - .Format = DXGI_FORMAT_R16G16B16A16_FLOAT, - .SampleDesc = { 1, 0 }, - .Usage = D3D11_USAGE_DEFAULT, - .BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS, - .CPUAccessFlags = 0, - .MiscFlags = 0 - }; - D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { - .Format = texDesc.Format, - .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, - .Texture2D = { - .MostDetailedMip = 0, - .MipLevels = texDesc.MipLevels } - }; - D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = { - .Format = texDesc.Format, - .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D, - .Texture2D = { .MipSlice = 0 } - }; - - diffuseIBLTexture = new Texture2D(texDesc); - diffuseIBLTexture->CreateSRV(srvDesc); - diffuseIBLTexture->CreateUAV(uavDesc); - } -} - -void WeatherEditor::ClearShaderCache() -{ - if (diffuseIBLCS) - diffuseIBLCS->Release(); - diffuseIBLCS = nullptr; -} - -ID3D11ComputeShader* WeatherEditor::GetDiffuseIBLCS() -{ - if (!diffuseIBLCS) - diffuseIBLCS = static_cast(Util::CompileShader(L"Data\\Shaders\\Weather\\DiffuseIBLCS.hlsl", {}, "cs_5_0")); - return diffuseIBLCS; -} void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeather, float currentWeatherPct) { diff --git a/src/Features/WeatherEditor.h b/src/Features/WeatherEditor.h index 2f471a3b70..6768aca20d 100644 --- a/src/Features/WeatherEditor.h +++ b/src/Features/WeatherEditor.h @@ -22,22 +22,13 @@ struct WeatherEditor : Feature return { "Development tool for editing weather, testing weather transitions, and managing weather-related feature settings.", { "Provides weather editing functionality", - "Includes dynamic saving and loading of imagespace, weather and lighting template settings.", + "Includes dynamic saving and loading of vanilla post processing and weather settings.", "Real-time editing and previewing of effects" } }; } - Texture2D* diffuseIBLTexture = nullptr; - ID3D11ComputeShader* diffuseIBLCS = nullptr; - - void Bind(); - virtual void DrawSettings() override; - virtual void Prepass() override; - virtual void SetupResources() override; - virtual void ClearShaderCache() override; - ID3D11ComputeShader* GetDiffuseIBLCS(); void LerpWeather(RE::TESWeather*, RE::TESWeather*, float); private: diff --git a/src/Utils/Form.cpp b/src/Utils/Form.cpp index 427c71eec1..683512d52f 100644 --- a/src/Utils/Form.cpp +++ b/src/Utils/Form.cpp @@ -15,6 +15,9 @@ auto Util::GetFormFromIdentifier(const std::string& a_identifier) -> RE::TESForm auto Util::GetIdentifierFromForm(const RE::TESForm* a_form) -> std::string { + if (a_form == nullptr) { + return "0|Null"; + } if (auto file = a_form->GetFile()) { return std::format("{:X}|{}", a_form->GetLocalFormID(), file->GetFilename()); } diff --git a/src/Utils/Serialize.cpp b/src/Utils/Serialize.cpp index 20f769d6b4..11ce659477 100644 --- a/src/Utils/Serialize.cpp +++ b/src/Utils/Serialize.cpp @@ -92,13 +92,13 @@ namespace nlohmann void from_json(const json& j, RE::TESWeather::FogData& fog) { std::array temp = j; - fog = { j[0], - j[1], - j[2], - j[3], - j[4], - j[5], - j[6], - j[7] }; + fog = { temp[0], + temp[1], + temp[2], + temp[3], + temp[4], + temp[5], + temp[6], + temp[7] }; } } \ No newline at end of file diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 6da10bd104..971bd2f98b 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include "../Feature.h" #include "../Globals.h" @@ -159,6 +162,85 @@ namespace Util logger::debug("LoadTextureFromFile: Successfully loaded {} ({}x{})", filename, image_width, image_height); return true; } + + bool LoadDDSTextureFromFile(ID3D11Device* device, + const char* filename, + ID3D11ShaderResourceView** out_srv, + ImVec2& out_size) + { + if (!device || !out_srv) { + logger::warn("LoadDDSTextureFromFile: Invalid parameters"); + return false; + } + + *out_srv = nullptr; + + // Try to load from BSA using Skyrim's resource system + RE::BSResourceNiBinaryStream bsaStream(filename); + if (!bsaStream.good()) { + logger::warn("LoadDDSTextureFromFile: Failed to open resource: {}", filename); + return false; + } + + // Read entire DDS file into memory + std::vector ddsData; + auto size = bsaStream.stream->totalSize; + if (size == 0) { + logger::warn("LoadDDSTextureFromFile: Resource has zero size: {}", filename); + return false; + } + + ddsData.resize(size); + bsaStream.read(reinterpret_cast(ddsData.data()), size); + + // Load DDS from memory + DirectX::ScratchImage image; + try { + DX::ThrowIfFailed(DirectX::LoadFromDDSMemory( + ddsData.data(), + ddsData.size(), + DirectX::DDS_FLAGS_NONE, + nullptr, + image)); + } catch (const DX::com_exception& e) { + logger::warn("LoadDDSTextureFromFile: Failed to load DDS data from {}: {}", filename, e.what()); + return false; + } + + ID3D11Resource* pResource = nullptr; + try { + DX::ThrowIfFailed(DirectX::CreateTexture(device, + image.GetImages(), image.GetImageCount(), + image.GetMetadata(), &pResource)); + } catch (const DX::com_exception& e) { + logger::warn("LoadDDSTextureFromFile: Failed to create texture: {}", e.what()); + return false; + } + + ID3D11Texture2D* pTexture = reinterpret_cast(pResource); + D3D11_TEXTURE2D_DESC desc; + pTexture->GetDesc(&desc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = desc.MipLevels } + }; + + HRESULT hr = device->CreateShaderResourceView(pTexture, &srvDesc, out_srv); + pTexture->Release(); + + if (FAILED(hr) || !*out_srv) { + logger::warn("LoadDDSTextureFromFile: Failed to create SRV, HRESULT: 0x{:08X}", static_cast(hr)); + return false; + } + + out_size = ImVec2((float)desc.Width, (float)desc.Height); + logger::debug("LoadDDSTextureFromFile: Successfully loaded {} ({}x{})", filename, desc.Width, desc.Height); + return true; + } bool InitializeMenuIcons(Menu* menu) { if (!menu) { @@ -264,7 +346,7 @@ namespace Util loadIcon(path, icon.texture, icon.size); } - logger::info("InitializeMenuIcons: Loaded {}/16 icons successfully", iconsLoaded); + logger::info("InitializeMenuIcons: {} icons successfully loaded", iconsLoaded); return anyIconLoaded; } @@ -1550,3 +1632,4 @@ namespace Util } } // namespace Util + diff --git a/src/Utils/UI.h b/src/Utils/UI.h index cd267ec055..18cb089247 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -250,6 +250,12 @@ namespace Util const char* filename, ID3D11ShaderResourceView** out_srv, ImVec2& out_size); + + bool LoadDDSTextureFromFile(ID3D11Device* device, + const char* filename, + ID3D11ShaderResourceView** out_srv, + ImVec2& out_size); + bool InitializeMenuIcons(Menu* menu); // Text rendering helpers for clearer title text diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 5297c19086..235de9bce3 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -4,9 +4,10 @@ #include "Features/WeatherEditor.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" +#include "PaletteWindow.h" #include "imgui_internal.h" -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, enableInheritFromParent, editorUIScale, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) void TextUnformattedDisabled(const char* a_text, const char* a_textEnd = nullptr) { @@ -36,7 +37,7 @@ inline void HelpMarker(const char* a_desc) AddTooltip(a_desc, ImGuiHoveredFlags_DelayShort); } -void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool filled) +void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool /*filled*/) { auto* drawList = ImGui::GetWindowDrawList(); const int numPoints = 5; @@ -49,19 +50,8 @@ void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool filled) points[i] = ImVec2(center.x + cosf(angle) * r, center.y + sinf(angle) * r); } - if (filled) { - // Draw filled star using triangles to avoid artifacts - for (int i = 0; i < numPoints; i++) { - int outerIdx1 = i * 2; - int innerIdx = (i * 2 + 1) % 10; - int outerIdx2 = (i * 2 + 2) % 10; - drawList->AddTriangleFilled(center, points[outerIdx1], points[innerIdx], color); - drawList->AddTriangleFilled(center, points[innerIdx], points[outerIdx2], color); - } - } else { - for (int i = 0; i < 10; i++) { - drawList->AddLine(points[i], points[(i + 1) % 10], color, 1.5f); - } + for (int i = 0; i < 10; i++) { + drawList->AddLine(points[i], points[(i + 1) % 10], color, 1.5f); } } @@ -164,26 +154,49 @@ void EditorWindow::ShowObjectsWindow() ImGui::Text("Categories"); ImGui::Spacing(); - // List of categories - const char* categories[] = { "Lighting Template", "Weather", "WorldSpace" }; - for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { - // Highlight the selected category - if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { - selectedCategory = categories[i]; // Update selected category - } + // List of categories + const char* categories[] = { "Lighting Template", "Weather", "WorldSpace", "Cell Lighting", "ImageSpace", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect" }; + for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { + // Highlight the selected category + if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { + selectedCategory = categories[i]; // Update selected category } - - // Right column: Objects - ImGui::TableSetColumnIndex(1); - - // Handle Ctrl+F to focus search bar - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { - if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { - ImGui::SetKeyboardFocusHere(); + } // Right column: Objects + ImGui::TableSetColumnIndex(1); + + // Display current active weather + auto sky = globals::game::sky; + if (sky && sky->currentWeather) { + auto currentWeather = sky->currentWeather; + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); + ImGui::Text("Current Active Weather:"); + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", currentWeather->GetFormEditorID()); + ImGui::SameLine(); + ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); + + // Add button to open the current weather + ImGui::SameLine(); + if (ImGui::SmallButton("Open##CurrentWeather")) { + for (auto& widget : weatherWidgets) { + if (widget->form == currentWeather) { + widget->SetOpen(true); + break; + } } } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + } - ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); + // Handle Ctrl+F to focus search bar + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(); + } + } ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); ImGui::SameLine(); HelpMarker("Type a part of an object name to filter the list.\nCtrl+F: Focus search\nEnter: Open selected"); @@ -207,40 +220,32 @@ void EditorWindow::ShowObjectsWindow() ImGui::SameLine(); ImGui::Text("Flagged"); - // Show recent widgets section if there are any - if (!settings.recentWidgets.empty()) { + // Show recent widgets section for current category + auto recentIt = settings.recentWidgets.find(selectedCategory); + if (recentIt != settings.recentWidgets.end() && !recentIt->second.empty()) { ImGui::Spacing(); ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Recent:"); ImGui::SameLine(); - for (size_t i = 0; i < std::min(size_t(5), settings.recentWidgets.size()); ++i) { + for (size_t i = 0; i < std::min(size_t(5), recentIt->second.size()); ++i) { if (i > 0) ImGui::SameLine(); - if (ImGui::SmallButton(settings.recentWidgets[i].c_str())) { - // Find and open widget in all collections - bool found = false; - for (auto* widget : weatherWidgets) { - if (widget->GetEditorID() == settings.recentWidgets[i]) { + if (ImGui::SmallButton(recentIt->second[i].c_str())) { + // Find and open widget in current category's collection + auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + selectedCategory == "Lighting Template" ? lightingTemplateWidgets : + selectedCategory == "ImageSpace" ? imageSpaceWidgets : + selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : + selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : + selectedCategory == "Lens Flare" ? lensFlareWidgets : + selectedCategory == "Visual Effect" ? referenceEffectWidgets : + weatherWidgets; + + for (auto& widget : widgets) { + if (widget->GetEditorID() == recentIt->second[i]) { widget->SetOpen(true); - found = true; break; } } - if (!found) { - for (auto* widget : worldSpaceWidgets) { - if (widget->GetEditorID() == settings.recentWidgets[i]) { - widget->SetOpen(true); - found = true; - break; - } - } - } - if (!found) { - for (auto* widget : lightingTemplateWidgets) { - if (widget->GetEditorID() == settings.recentWidgets[i]) { - widget->SetOpen(true); - break; - } - } - } } } } @@ -269,13 +274,23 @@ void EditorWindow::ShowObjectsWindow() } } - // Display objects based on the selected category - auto& widgets = selectedCategory == "Weather" ? weatherWidgets : - selectedCategory == "WorldSpace" ? worldSpaceWidgets : - lightingTemplateWidgets; - + // Display objects based on the selected category + std::vector> emptyWidgets; + const auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + selectedCategory == "Cell Lighting" ? emptyWidgets : + selectedCategory == "ImageSpace" ? imageSpaceWidgets : + selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : + selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : + selectedCategory == "Lens Flare" ? lensFlareWidgets : + selectedCategory == "Visual Effect" ? referenceEffectWidgets : + lightingTemplateWidgets; // Sort widgets based on current sort column - std::vector sortedWidgets = widgets; + std::vector sortedWidgets; + sortedWidgets.reserve(widgets.size()); + for (const auto& w : widgets) { + sortedWidgets.push_back(w.get()); + } if (currentSortColumn != SortColumn::None) { std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { int comparison = 0; @@ -304,6 +319,82 @@ void EditorWindow::ShowObjectsWindow() }); } + // Special handling for Cell Lighting category + if (selectedCategory == "Cell Lighting") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto cell = player->parentCell; + bool isInterior = cell->IsInteriorCell(); + + if (isInterior) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + // No favorite star for cell lighting (it's always the current cell) + ImGui::Dummy(ImVec2(24, 24)); + + ImGui::TableNextColumn(); + + // Display current cell name + const char* cellName = cell->GetName(); + std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; + std::string label = std::format("[CURRENT CELL] {}", displayName); + + bool isOpen = currentCellLightingWidget && currentCellLightingWidget->IsOpen(); + if (ImGui::Selectable(label.c_str(), isOpen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + // Open or reuse the cell lighting widget + if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { + currentCellLightingWidget->SetOpen(true); + } else { + currentCellLightingWidget = std::make_unique(cell); + currentCellLightingWidget->CacheFormData(); + currentCellLightingWidget->Load(); + currentCellLightingWidget->SetOpen(true); + } + } + } + + // Highlight current cell + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + + // Enter key to open + if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { + currentCellLightingWidget->SetOpen(true); + } + } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", cell->GetFormID()); + + // File column + ImGui::TableNextColumn(); + auto file = cell->GetFile(0); + if (file) { + ImGui::Text("%s", file->fileName); + } + + // Status column + ImGui::TableNextColumn(); + ImGui::Text("Interior Cell"); + } else { + // Show message that cell lighting is only for interior cells + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "Cell Lighting is only available for interior cells."); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "You are currently in an exterior cell."); + } + } else { + // No player or cell + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Player cell not available."); + } + } + // Get current cell's lighting template for prioritization RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; if (selectedCategory == "Lighting Template") { @@ -352,13 +443,44 @@ void EditorWindow::ShowObjectsWindow() if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } } + + // Show ImageSpace and VolumetricLighting info for weather widgets + if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { + auto* weatherWidget = dynamic_cast(sortedWidgets[i]); + if (weatherWidget && weatherWidget->weather) { + ImGui::BeginTooltip(); + + // ImageSpace info + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "ImageSpace:"); + for (int tod = 0; tod < 4; tod++) { + auto imgSpace = weatherWidget->weather->imageSpaces[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + imgSpace ? imgSpace->GetFormEditorID() : "None"); + } + + ImGui::Spacing(); + + // VolumetricLighting info + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Volumetric Lighting:"); + for (int tod = 0; tod < 4; tod++) { + auto volLight = weatherWidget->weather->volumetricLighting[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + volLight ? volLight->GetFormEditorID() : "None"); + } + + ImGui::EndTooltip(); + } + } + // Enter key to open if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } // Context menu @@ -439,13 +561,13 @@ void EditorWindow::ShowObjectsWindow() if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } } // Enter key to open if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } // Opens a context menu on right click to mark records by color @@ -499,8 +621,12 @@ void EditorWindow::ShowViewportWindow() // Top bar auto calendar = RE::Calendar::GetSingleton(); - if (calendar) + if (calendar && calendar->gameHour) { ImGui::SliderFloat("##ViewportSlider", &calendar->gameHour->value, 0.0f, 23.99f, "Time: %.2f"); + ImGui::SameLine(); + int activePeriod = TOD::GetActivePeriod(); + ImGui::Text("(%s)", TOD::GetPeriodName(activePeriod)); + } // The size of the image in ImGui // Get the available space in the current window ImVec2 availableSpace = ImGui::GetContentRegionAvail(); @@ -531,28 +657,56 @@ void EditorWindow::ShowWidgetWindow() if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_W, false)) { // Close the most recently focused widget for (int i = 0; i < (int)weatherWidgets.size(); i++) { - auto widget = weatherWidgets[i]; + auto& widget = weatherWidgets[i]; if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { widget->SetOpen(false); break; } } for (int i = 0; i < (int)worldSpaceWidgets.size(); i++) { - auto widget = worldSpaceWidgets[i]; + auto& widget = worldSpaceWidgets[i]; if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { widget->SetOpen(false); break; } } for (int i = 0; i < (int)lightingTemplateWidgets.size(); i++) { - auto widget = lightingTemplateWidgets[i]; + auto& widget = lightingTemplateWidgets[i]; if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { widget->SetOpen(false); break; } } for (int i = 0; i < (int)imageSpaceWidgets.size(); i++) { - auto widget = imageSpaceWidgets[i]; + auto& widget = imageSpaceWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + for (int i = 0; i < (int)volumetricLightingWidgets.size(); i++) { + auto& widget = volumetricLightingWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + for (int i = 0; i < (int)precipitationWidgets.size(); i++) { + auto& widget = precipitationWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + for (int i = 0; i < (int)lensFlareWidgets.size(); i++) { + auto& widget = lensFlareWidgets[i]; + if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { + widget->SetOpen(false); + break; + } + } + for (int i = 0; i < (int)referenceEffectWidgets.size(); i++) { + auto& widget = referenceEffectWidgets[i]; if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { widget->SetOpen(false); break; @@ -561,28 +715,57 @@ void EditorWindow::ShowWidgetWindow() } for (int i = 0; i < (int)weatherWidgets.size(); i++) { - auto widget = weatherWidgets[i]; + auto& widget = weatherWidgets[i]; if (widget->IsOpen()) widget->DrawWidget(); } for (int i = 0; i < (int)worldSpaceWidgets.size(); i++) { - auto widget = worldSpaceWidgets[i]; + auto& widget = worldSpaceWidgets[i]; if (widget->IsOpen()) widget->DrawWidget(); } for (int i = 0; i < (int)lightingTemplateWidgets.size(); i++) { - auto widget = lightingTemplateWidgets[i]; + auto& widget = lightingTemplateWidgets[i]; if (widget->IsOpen()) widget->DrawWidget(); } for (int i = 0; i < (int)imageSpaceWidgets.size(); i++) { - auto widget = imageSpaceWidgets[i]; + auto& widget = imageSpaceWidgets[i]; if (widget->IsOpen()) widget->DrawWidget(); } + + for (int i = 0; i < (int)volumetricLightingWidgets.size(); i++) { + auto& widget = volumetricLightingWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } + + for (int i = 0; i < (int)precipitationWidgets.size(); i++) { + auto& widget = precipitationWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } + + for (int i = 0; i < (int)lensFlareWidgets.size(); i++) { + auto& widget = lensFlareWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } + + for (int i = 0; i < (int)referenceEffectWidgets.size(); i++) { + auto& widget = referenceEffectWidgets[i]; + if (widget->IsOpen()) + widget->DrawWidget(); + } + + // Draw current cell lighting widget if open + if (currentCellLightingWidget && currentCellLightingWidget->IsOpen()) { + currentCellLightingWidget->DrawWidget(); + } } void EditorWindow::RenderUI() @@ -593,6 +776,11 @@ void EditorWindow::RenderUI() context->ClearRenderTargetView(framebuffer.RTV, (float*)&ImGui::GetStyle().Colors[ImGuiCol_WindowBg]); + // Apply editor UI scale + ImGuiIO& io = ImGui::GetIO(); + float previousScale = io.FontGlobalScale; + io.FontGlobalScale = settings.editorUIScale; + // Increase background opacity for all editor windows ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); @@ -612,40 +800,40 @@ void EditorWindow::RenderUI() bool hasOpenWidgets = false; // Weather widgets - for (auto* widget : weatherWidgets) { + for (auto& widget : weatherWidgets) { if (widget->IsOpen()) { hasOpenWidgets = true; - if (ImGui::MenuItem(std::format("Save Weather_{}", widget->GetEditorID()).c_str())) { + if (ImGui::MenuItem(std::format("Save {}", widget->GetEditorID()).c_str())) { widget->Save(); } } } // WorldSpace widgets - for (auto* widget : worldSpaceWidgets) { + for (auto& widget : worldSpaceWidgets) { if (widget->IsOpen()) { hasOpenWidgets = true; - if (ImGui::MenuItem(std::format("Save WorldSpace_{}", widget->GetEditorID()).c_str())) { + if (ImGui::MenuItem(std::format("Save {}", widget->GetEditorID()).c_str())) { widget->Save(); } } } // Lighting Template widgets - for (auto* widget : lightingTemplateWidgets) { + for (auto& widget : lightingTemplateWidgets) { if (widget->IsOpen()) { hasOpenWidgets = true; - if (ImGui::MenuItem(std::format("Save Lighting_{}", widget->GetEditorID()).c_str())) { + if (ImGui::MenuItem(std::format("Save {}", widget->GetEditorID()).c_str())) { widget->Save(); } } } // ImageSpace widgets - for (auto* widget : imageSpaceWidgets) { + for (auto& widget : imageSpaceWidgets) { if (widget->IsOpen()) { hasOpenWidgets = true; - if (ImGui::MenuItem(std::format("Save ImageSpace_{}", widget->GetEditorID()).c_str())) { + if (ImGui::MenuItem(std::format("Save {}", widget->GetEditorID()).c_str())) { widget->Save(); } } @@ -660,24 +848,60 @@ void EditorWindow::RenderUI() ImGui::Separator(); if (ImGui::MenuItem("Close All Weather Widgets")) { - for (auto* widget : weatherWidgets) widget->SetOpen(false); + for (auto& widget : weatherWidgets) widget->SetOpen(false); } if (ImGui::MenuItem("Close All WorldSpace Widgets")) { - for (auto* widget : worldSpaceWidgets) widget->SetOpen(false); + for (auto& widget : worldSpaceWidgets) widget->SetOpen(false); } if (ImGui::MenuItem("Close All Lighting Widgets")) { - for (auto* widget : lightingTemplateWidgets) widget->SetOpen(false); + for (auto& widget : lightingTemplateWidgets) widget->SetOpen(false); } if (ImGui::MenuItem("Close All ImageSpace Widgets")) { - for (auto* widget : imageSpaceWidgets) widget->SetOpen(false); + for (auto& widget : imageSpaceWidgets) widget->SetOpen(false); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Settings")) { + if (ImGui::MenuItem("General Settings")) { + showSettingsWindow = true; + settingsSelectedCategory = "General"; + } if (ImGui::MenuItem("Editor Flags")) { - showSettingsWindow = !showSettingsWindow; + showSettingsWindow = true; + settingsSelectedCategory = "Flags"; } ImGui::Separator(); + + // Current cell lighting + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell && player->parentCell->IsInteriorCell()) { + if (ImGui::MenuItem("Edit Current Cell Lighting")) { + // Check if widget already exists + bool found = false; + if (currentCellLightingWidget && currentCellLightingWidget->cell == player->parentCell) { + currentCellLightingWidget->SetOpen(true); + found = true; + } + + if (!found) { + // Create new widget for current cell + currentCellLightingWidget = std::make_unique(player->parentCell); + currentCellLightingWidget->CacheFormData(); + currentCellLightingWidget->Load(); + currentCellLightingWidget->SetOpen(true); + } + } + } else { + ImGui::BeginDisabled(); + ImGui::MenuItem("Edit Current Cell Lighting"); + ImGui::EndDisabled(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { + ImGui::SetTooltip("Only available in interior cells"); + } + } + + ImGui::Separator(); + if (ImGui::Checkbox("Auto-Apply Changes", &settings.autoApplyChanges)) { Save(); } @@ -690,14 +914,25 @@ void EditorWindow::RenderUI() if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Restore previously open widgets when editor reopens"); } + if (ImGui::Checkbox("Enable Inherit From Parent", &settings.enableInheritFromParent)) { + Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Show inherit from parent options in weather widgets"); + } ImGui::EndMenu(); } if (ImGui::BeginMenu("Window")) { + if (ImGui::MenuItem("Palette", nullptr, PaletteWindow::GetSingleton()->open)) { + PaletteWindow::GetSingleton()->open = !PaletteWindow::GetSingleton()->open; + } + + ImGui::Separator(); ImGui::Text("Open Widgets:"); ImGui::Separator(); int openCount = 0; - for (auto* widget : weatherWidgets) { + for (auto& widget : weatherWidgets) { if (widget->IsOpen()) { openCount++; if (ImGui::MenuItem(std::format("Weather: {}", widget->GetEditorID()).c_str())) { @@ -705,7 +940,7 @@ void EditorWindow::RenderUI() } } } - for (auto* widget : worldSpaceWidgets) { + for (auto& widget : worldSpaceWidgets) { if (widget->IsOpen()) { openCount++; if (ImGui::MenuItem(std::format("WorldSpace: {}", widget->GetEditorID()).c_str())) { @@ -713,7 +948,7 @@ void EditorWindow::RenderUI() } } } - for (auto* widget : lightingTemplateWidgets) { + for (auto& widget : lightingTemplateWidgets) { if (widget->IsOpen()) { openCount++; if (ImGui::MenuItem(std::format("Lighting: {}", widget->GetEditorID()).c_str())) { @@ -721,7 +956,7 @@ void EditorWindow::RenderUI() } } } - for (auto* widget : imageSpaceWidgets) { + for (auto& widget : imageSpaceWidgets) { if (widget->IsOpen()) { openCount++; if (ImGui::MenuItem(std::format("ImageSpace: {}", widget->GetEditorID()).c_str())) { @@ -761,7 +996,13 @@ void EditorWindow::RenderUI() ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Favorites: %d", (int)settings.favoriteWidgets.size()); - ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "Recent: %d", (int)settings.recentWidgets.size()); + + // Count total recent widgets across all categories + int totalRecent = 0; + for (const auto& [category, widgets] : settings.recentWidgets) { + totalRecent += static_cast(widgets.size()); + } + ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "Recent: %d", totalRecent); ImGui::EndMenu(); } @@ -816,21 +1057,20 @@ void EditorWindow::RenderUI() ImGui::PopStyleColor(); } - // Close button on the right side - float menuBarHeight = ImGui::GetFrameHeight(); - ImGui::SameLine(ImGui::GetWindowWidth() - menuBarHeight - 10.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); - if (ImGui::Button("X", ImVec2(menuBarHeight, menuBarHeight))) { - open = false; - } - ImGui::PopStyleColor(3); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Close Weather Editor (Esc)"); - } - - ImGui::EndMainMenuBar(); + // Close button on the right side + float menuBarHeight = ImGui::GetFrameHeight(); + float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar + ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { + open = false; + } + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Close Weather Editor (Esc)"); + } ImGui::EndMainMenuBar(); } auto width = ImGui::GetIO().DisplaySize.x; @@ -852,12 +1092,33 @@ void EditorWindow::RenderUI() } ShowWidgetWindow(); + + // Show palette window + PaletteWindow::GetSingleton()->Draw(); // Render notifications on top of everything RenderNotifications(); // Pop the alpha style var ImGui::PopStyleVar(); + + // Restore previous font scale + io.FontGlobalScale = previousScale; +} + +EditorWindow::~EditorWindow() +{ + delete tempTexture; + weatherWidgets.clear(); + worldSpaceWidgets.clear(); + lightingTemplateWidgets.clear(); + imageSpaceWidgets.clear(); + volumetricLightingWidgets.clear(); + precipitationWidgets.clear(); + referenceEffectWidgets.clear(); + artObjectWidgets.clear(); + effectShaderWidgets.clear(); + currentCellLightingWidget.reset(); } void EditorWindow::SetupResources() @@ -866,35 +1127,94 @@ void EditorWindow::SetupResources() auto& weatherArray = dataHandler->GetFormArray(); Load(); + PaletteWindow::GetSingleton()->Load(); for (auto weather : weatherArray) { - auto widget = new WeatherWidget(weather); + auto widget = std::make_unique(weather); + widget->CacheFormData(); widget->Load(); - weatherWidgets.push_back(widget); + weatherWidgets.push_back(std::move(widget)); } auto& worldSpaceArray = dataHandler->GetFormArray(); for (auto worldSpace : worldSpaceArray) { - auto widget = new WorldSpaceWidget(worldSpace); + auto widget = std::make_unique(worldSpace); + widget->CacheFormData(); widget->Load(); - worldSpaceWidgets.push_back(widget); + worldSpaceWidgets.push_back(std::move(widget)); } auto& lightingTemplateArray = dataHandler->GetFormArray(); for (auto lightingTemplate : lightingTemplateArray) { - auto widget = new LightingTemplateWidget(lightingTemplate); + auto widget = std::make_unique(lightingTemplate); + widget->CacheFormData(); widget->Load(); - lightingTemplateWidgets.push_back(widget); + lightingTemplateWidgets.push_back(std::move(widget)); } auto& imageSpaceArray = dataHandler->GetFormArray(); for (auto imageSpace : imageSpaceArray) { - auto widget = new ImageSpaceWidget(imageSpace); + auto widget = std::make_unique(imageSpace); + widget->CacheFormData(); + widget->Load(); + imageSpaceWidgets.push_back(std::move(widget)); + } + + auto& volumetricLightingArray = dataHandler->GetFormArray(); + + for (auto volumetricLighting : volumetricLightingArray) { + auto widget = std::make_unique(volumetricLighting); + widget->CacheFormData(); + widget->Load(); + volumetricLightingWidgets.push_back(std::move(widget)); + } + + auto& precipitationArray = dataHandler->GetFormArray(); + + for (auto precipitation : precipitationArray) { + auto widget = std::make_unique(precipitation); + widget->CacheFormData(); + widget->Load(); + precipitationWidgets.push_back(std::move(widget)); + } + + auto& lensFlareArray = dataHandler->GetFormArray(); + + for (auto lensFlare : lensFlareArray) { + auto widget = std::make_unique(lensFlare); + widget->CacheFormData(); + widget->Load(); + lensFlareWidgets.push_back(std::move(widget)); + } + + auto& referenceEffectArray = dataHandler->GetFormArray(); + + for (auto referenceEffect : referenceEffectArray) { + auto widget = std::make_unique(referenceEffect); + widget->CacheFormData(); widget->Load(); - imageSpaceWidgets.push_back(widget); + referenceEffectWidgets.push_back(std::move(widget)); + } + + // Cache art objects for form picker performance + auto& artObjectArray = dataHandler->GetFormArray(); + for (auto artObject : artObjectArray) { + auto widget = std::make_unique(); + widget->form = artObject; + widget->CacheFormData(); + artObjectWidgets.push_back(std::move(widget)); + } + + // Cache effect shaders for form picker performance + auto& effectShaderArray = dataHandler->GetFormArray(); + for (auto effectShader : effectShaderArray) { + auto widget = std::make_unique(); + widget->form = effectShader; + widget->CacheFormData(); + effectShaderWidgets.push_back(std::move(widget)); } } @@ -926,7 +1246,7 @@ void EditorWindow::Draw() auto renderer = RE::BSGraphics::Renderer::GetSingleton(); auto& framebuffer = renderer->GetRuntimeData().renderTargets[RE::RENDER_TARGETS::kFRAMEBUFFER]; - ID3D11Resource* resource; + ID3D11Resource* resource = nullptr; framebuffer.SRV->GetResource(&resource); if (!tempTexture) { @@ -944,27 +1264,31 @@ void EditorWindow::Draw() context->CopyResource(tempTexture->resource.get(), resource); + if (resource) { + resource->Release(); + } + RenderUI(); } void EditorWindow::SaveAll() { - for (auto weather : weatherWidgets) { + for (auto& weather : weatherWidgets) { if (weather->IsOpen()) weather->Save(); } - for (auto worldspace : worldSpaceWidgets) { + for (auto& worldspace : worldSpaceWidgets) { if (worldspace->IsOpen()) worldspace->Save(); } - for (auto lightingTemplate : lightingTemplateWidgets) { + for (auto& lightingTemplate : lightingTemplateWidgets) { if (lightingTemplate->IsOpen()) lightingTemplate->Save(); } - for (auto imageSpace : imageSpaceWidgets) { + for (auto& imageSpace : imageSpaceWidgets) { if (imageSpace->IsOpen()) imageSpace->Save(); } @@ -987,31 +1311,23 @@ void EditorWindow::ShowSettingsWindow() { ImGui::Begin("Settings", &showSettingsWindow); - // Static variable to track the selected category - static std::string selectedOption = "Flags"; - - // Create a table with two columns if (ImGui::BeginTable("SettingsTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInner | ImGuiTableFlags_NoHostExtendX)) { - // Set up column widths - ImGui::TableSetupColumn("Options", ImGuiTableColumnFlags_WidthStretch, 0.3f); // 30% width - ImGui::TableSetupColumn("##Settings", ImGuiTableColumnFlags_WidthStretch, 0.7f); // 70% width + ImGui::TableSetupColumn("Options", ImGuiTableColumnFlags_WidthStretch, 0.3f); + ImGui::TableSetupColumn("##Settings", ImGuiTableColumnFlags_WidthStretch, 0.7f); ImGui::TableNextRow(); - // Left column: Options ImGui::TableSetColumnIndex(0); - // List of options const char* options[] = { "General", "Flags" }; for (int i = 0; i < IM_ARRAYSIZE(options); ++i) { - if (ImGui::Selectable(options[i], selectedOption == options[i])) { - selectedOption = options[i]; // Update selected option + if (ImGui::Selectable(options[i], settingsSelectedCategory == options[i])) { + settingsSelectedCategory = options[i]; } } - // Right column: Option settings ImGui::TableSetColumnIndex(1); - if (selectedOption == "General") { + if (settingsSelectedCategory == "General") { ImGui::Checkbox("Auto-apply changes", &settings.autoApplyChanges); AddTooltip("Automatically apply changes to weather/lighting when editing"); @@ -1021,6 +1337,25 @@ void EditorWindow::ShowSettingsWindow() ImGui::Checkbox("Use text buttons instead of icons", &settings.useTextButtons); AddTooltip("Display action buttons as text labels instead of icons"); + ImGui::Checkbox("Enable 'Inherit From Parent' feature", &settings.enableInheritFromParent); + AddTooltip("Show checkboxes to copy settings from parent weather (editor-only feature)"); + + ImGui::Separator(); + ImGui::TextUnformatted("UI Scale"); + ImGui::Spacing(); + + if (ImGui::SliderFloat("Editor UI Scale", &settings.editorUIScale, 0.5f, 2.0f, "%.2f")) { + Save(); + } + AddTooltip("Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)"); + + if (ImGui::Button("Reset to 1.0")) { + settings.editorUIScale = 1.0f; + Save(); + } + ImGui::SameLine(); + AddTooltip("Reset UI scale to default (100%)"); + ImGui::Separator(); ImGui::TextUnformatted("Session & History"); ImGui::Spacing(); @@ -1041,7 +1376,7 @@ void EditorWindow::ShowSettingsWindow() Save(); } - } else if (selectedOption == "Flags") { + } else if (settingsSelectedCategory == "Flags") { if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); ImGui::TableSetupColumn("Colour", ImGuiTableColumnFlags_WidthStretch); @@ -1340,20 +1675,22 @@ void EditorWindow::RenderNotifications() } } -void EditorWindow::AddToRecent(const std::string& widgetId) +void EditorWindow::AddToRecent(const std::string& widgetId, const std::string& category) { + auto& categoryRecent = settings.recentWidgets[category]; + // Remove if already exists - auto it = std::find(settings.recentWidgets.begin(), settings.recentWidgets.end(), widgetId); - if (it != settings.recentWidgets.end()) { - settings.recentWidgets.erase(it); + auto it = std::find(categoryRecent.begin(), categoryRecent.end(), widgetId); + if (it != categoryRecent.end()) { + categoryRecent.erase(it); } // Add to front - settings.recentWidgets.insert(settings.recentWidgets.begin(), widgetId); + categoryRecent.insert(categoryRecent.begin(), widgetId); // Limit size - if (settings.recentWidgets.size() > static_cast(settings.maxRecentWidgets)) { - settings.recentWidgets.resize(settings.maxRecentWidgets); + if (categoryRecent.size() > static_cast(settings.maxRecentWidgets)) { + categoryRecent.resize(settings.maxRecentWidgets); } SaveSettings(); @@ -1380,17 +1717,17 @@ void EditorWindow::SaveSessionWidgets() settings.lastOpenWidgets.clear(); // Save all currently open widgets - for (auto widget : weatherWidgets) { + for (auto& widget : weatherWidgets) { if (widget->IsOpen()) { settings.lastOpenWidgets.push_back(widget->GetEditorID()); } } - for (auto widget : worldSpaceWidgets) { + for (auto& widget : worldSpaceWidgets) { if (widget->IsOpen()) { settings.lastOpenWidgets.push_back(widget->GetEditorID()); } } - for (auto widget : lightingTemplateWidgets) { + for (auto& widget : lightingTemplateWidgets) { if (widget->IsOpen()) { settings.lastOpenWidgets.push_back(widget->GetEditorID()); } @@ -1408,19 +1745,19 @@ void EditorWindow::RestoreSessionWidgets() // Open widgets that were open in last session for (const auto& widgetId : settings.lastOpenWidgets) { // Search in all widget collections - for (auto widget : weatherWidgets) { + for (auto& widget : weatherWidgets) { if (widget->GetEditorID() == widgetId) { widget->SetOpen(true); break; } } - for (auto widget : worldSpaceWidgets) { + for (auto& widget : worldSpaceWidgets) { if (widget->GetEditorID() == widgetId) { widget->SetOpen(true); break; } } - for (auto widget : lightingTemplateWidgets) { + for (auto& widget : lightingTemplateWidgets) { if (widget->GetEditorID() == widgetId) { widget->SetOpen(true); break; diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index f735193e71..55526edfe2 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -2,8 +2,13 @@ #include "Buffer.h" +#include "Weather/CellLightingWidget.h" #include "Weather/ImageSpaceWidget.h" +#include "Weather/LensFlareWidget.h" #include "Weather/LightingTemplateWidget.h" +#include "Weather/PrecipitationWidget.h" +#include "Weather/ReferenceEffectWidget.h" +#include "Weather/VolumetricLightingWidget.h" #include "Weather/WeatherWidget.h" #include "Weather/WorldSpaceWidget.h" #include "WeatherUtils.h" @@ -21,12 +26,23 @@ class EditorWindow bool open = false; const static int maxRecordMarkers = 10; - Texture2D* tempTexture; + // Owned by EditorWindow, created in Draw(), released in destructor + Texture2D* tempTexture = nullptr; - std::vector weatherWidgets; - std::vector worldSpaceWidgets; - std::vector lightingTemplateWidgets; - std::vector imageSpaceWidgets; + // Widget collections owned by EditorWindow, created in SetupResources(), released in destructor + std::vector> weatherWidgets; + std::vector> worldSpaceWidgets; + std::vector> lightingTemplateWidgets; + std::vector> imageSpaceWidgets; + std::vector> volumetricLightingWidgets; + std::vector> precipitationWidgets; + std::vector> lensFlareWidgets; + std::vector> referenceEffectWidgets; + std::vector> artObjectWidgets; + std::vector> effectShaderWidgets; + + // Owned by EditorWindow, created on demand in ShowObjectsWindow(), released in destructor + std::unique_ptr currentCellLightingWidget; // Weather locking for editing RE::TESWeather* lockedWeather = nullptr; @@ -85,25 +101,51 @@ class EditorWindow { "Complete", { 0.0f, 130.0f / 255.0f, 0.0f, 1.0f } } }; std::map markedRecords; - bool autoApplyChanges = true; - bool suppressDeleteWarning = false; - bool useTextButtons = false; + bool autoApplyChanges = true; + bool suppressDeleteWarning = false; + bool useTextButtons = false; + bool enableInheritFromParent = false; + float editorUIScale = 1.0f; std::vector favoriteWidgets; - std::vector recentWidgets; + std::map> recentWidgets; int maxRecentWidgets = 10; bool rememberOpenWidgets = true; std::vector lastOpenWidgets; + + // Palette settings + struct PaletteColorEntry { + float r, g, b; + int useCount = 0; + float lastUsedTime = 0.0f; + bool isFavorite = false; + }; + struct PaletteValueEntry { + std::string name; + float value; + int useCount = 0; + float lastUsedTime = 0.0f; + bool isFavorite = false; + }; + struct PaletteFavoriteColor { + bool hasValue = false; + float r = 0.0f, g = 0.0f, b = 0.0f; + }; + std::vector paletteColors; + std::vector paletteValues; + std::array paletteFavorites; }; Settings settings; void Save(); - void AddToRecent(const std::string& widgetId); + void AddToRecent(const std::string& widgetId, const std::string& category); void ToggleFavorite(const std::string& widgetId); bool IsFavorite(const std::string& widgetId) const; void SaveSessionWidgets(); void RestoreSessionWidgets(); + ~EditorWindow(); + private: void SaveAll(); void SaveSettings(); @@ -113,6 +155,7 @@ class EditorWindow json j; std::string settingsFilename = "EditorSettings"; bool showSettingsWindow = false; + std::string settingsSelectedCategory = "Flags"; // Sorting state enum class SortColumn { None, EditorID, FormID, File, Status }; diff --git a/src/WeatherEditor/PaletteWindow.cpp b/src/WeatherEditor/PaletteWindow.cpp new file mode 100644 index 0000000000..2d1aa1c38a --- /dev/null +++ b/src/WeatherEditor/PaletteWindow.cpp @@ -0,0 +1,495 @@ +#include "PaletteWindow.h" +#include "EditorWindow.h" + +// Forward declaration from EditorWindow.cpp +void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool filled); + +void PaletteWindow::Draw() +{ + if (!open) + return; + + ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - 620, ImGui::GetIO().DisplaySize.y - 420), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Palette", &open)) { + if (ImGui::BeginTabBar("PaletteTabs")) { + if (ImGui::BeginTabItem("Colours")) { + DrawColorsTab(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Values")) { + DrawValuesTab(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + } + ImGui::End(); +} + +void PaletteWindow::DrawColorsTab() +{ + const float buttonSize = 32.0f; + const float spacing = 8.0f; + + // Favorites section at top + ImGui::SeparatorText("Favourites"); + ImGui::TextWrapped("Drag colours here to save as favourites."); + ImGui::Spacing(); + + for (int i = 0; i < maxFavoriteSlots; i++) { + if (i > 0) + ImGui::SameLine(0.0f, spacing); + + std::string id = "##favorite_" + std::to_string(i); + + if (favoriteColors[i].has_value()) { + // Show filled favorite slot + auto& color = favoriteColors[i].value(); + ImVec4 colorVec(color.x, color.y, color.z, 1.0f); + + if (ImGui::ColorButton(id.c_str(), colorVec, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { + copiedColor = color; + hasColorInClipboard = true; + ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", color.x, color.y, color.z).c_str()); + } + + // Drag source + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload("COLOR_DND", &color, sizeof(float3)); + ImGui::ColorButton("##preview", colorVec, ImGuiColorEditFlags_NoAlpha); + ImGui::EndDragDropSource(); + } + + // Right-click to clear + if (ImGui::BeginPopupContextItem()) { + if (ImGui::Selectable("Clear favourite")) { + favoriteColors[i].reset(); + Save(); + } + ImGui::EndPopup(); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nClick to copy\nRight-click to clear", color.x, color.y, color.z); + } + } else { + // Show empty favorite slot with star + ImVec4 emptyColor(0.2f, 0.2f, 0.2f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Button, emptyColor); + ImGui::Button(id.c_str(), ImVec2(buttonSize, buttonSize)); + ImGui::PopStyleColor(); + + // Draw star icon in center + ImVec2 buttonMin = ImGui::GetItemRectMin(); + ImVec2 buttonMax = ImGui::GetItemRectMax(); + ImVec2 center = ImVec2((buttonMin.x + buttonMax.x) * 0.5f, (buttonMin.y + buttonMax.y) * 0.5f); + float starSize = buttonSize * 0.4f; + ImU32 starColor = IM_COL32(160, 160, 160, 255); + + DrawIconStar(center, starSize, starColor, false); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Drag a colour here to add to favourites"); + } + } + + // Drag-and-drop target + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR_DND")) { + float3 droppedColor; + memcpy(&droppedColor, payload->Data, sizeof(float3)); + favoriteColors[i] = droppedColor; + Save(); + } + ImGui::EndDragDropTarget(); + } + } + ImGui::Spacing(); + ImGui::Spacing(); + + // Recently Used section + ImGui::SeparatorText("Recently Used"); + auto recentColors = GetRecentColors(5); + + if (recentColors.empty()) { + ImGui::TextDisabled("No recent colors"); + } else { + for (size_t i = 0; i < recentColors.size(); i++) { + if (i > 0) + ImGui::SameLine(0.0f, spacing); + + auto* entry = recentColors[i]; + ImVec4 color(entry->color.x, entry->color.y, entry->color.z, 1.0f); + std::string id = "##recent_color_" + std::to_string(i); + + if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { + copiedColor = entry->color; + hasColorInClipboard = true; + ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", entry->color.x, entry->color.y, entry->color.z).c_str()); + } + + // Drag source + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload("COLOR_DND", &entry->color, sizeof(float3)); + ImGui::ColorButton("##preview", color, ImGuiColorEditFlags_NoAlpha); + ImGui::EndDragDropSource(); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nUsed %d times\nClick to copy", + entry->color.x, entry->color.y, entry->color.z, entry->useCount); + } + } + } + ImGui::Spacing(); + + // Most Used section + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextUnformatted("Most Used"); + ImGui::Spacing(); + ImGui::TextWrapped("Favourite/most commonly used colours here."); + ImGui::Spacing(); + + auto mostUsedColors = GetMostUsedColors(20); + + if (mostUsedColors.empty()) { + ImGui::TextDisabled("No frequently used colors yet"); + ImGui::TextDisabled("(Colors used 3+ times will appear here)"); + } else { + int colorIndex = 0; + for (auto* entry : mostUsedColors) { + if (colorIndex > 0 && colorIndex % 10 != 0) + ImGui::SameLine(0.0f, spacing); + + ImVec4 color(entry->color.x, entry->color.y, entry->color.z, 1.0f); + std::string id = "##mostused_color_" + std::to_string(colorIndex); + + if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { + copiedColor = entry->color; + hasColorInClipboard = true; + ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", entry->color.x, entry->color.y, entry->color.z).c_str()); + } + + // Drag source + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload("COLOR_DND", &entry->color, sizeof(float3)); + ImGui::ColorButton("##preview", color, ImGuiColorEditFlags_NoAlpha); + ImGui::EndDragDropSource(); + } + + // Right-click to remove + if (ImGui::BeginPopupContextItem()) { + if (ImGui::Selectable("Remove from palette")) { + auto it = std::find_if(colorEntries.begin(), colorEntries.end(), + [entry](const ColorEntry& e) { return &e == entry; }); + if (it != colorEntries.end()) { + colorEntries.erase(it); + Save(); + } + } + ImGui::EndPopup(); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nUsed %d times\nClick to copy\nRight-click to remove", + entry->color.x, entry->color.y, entry->color.z, entry->useCount); + } + + colorIndex++; + } + } +} + +void PaletteWindow::DrawValuesTab() +{ + // Recently Used section + ImGui::SeparatorText("Recently Used"); + auto recentValues = GetRecentValues(3); + if (recentValues.empty()) { + ImGui::TextDisabled("No recent values"); + } else { + for (auto* entry : recentValues) { + std::string label = std::format("{}: {:.3f}", entry->name, entry->value); + if (ImGui::Selectable(label.c_str())) { + copiedValue = entry->value; + copiedValueName = entry->name; + hasValueInClipboard = true; + ImGui::SetClipboardText(std::to_string(entry->value).c_str()); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Used %d times\nClick to copy", entry->useCount); + } + } + } + ImGui::Spacing(); + // Most Used section + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextUnformatted("Most Used"); + ImGui::Spacing(); + ImGui::TextWrapped("Favourite/most commonly used values here."); + ImGui::Spacing(); + + auto mostUsedValues = GetMostUsedValues(20); + + if (mostUsedValues.empty()) { + ImGui::TextDisabled("No frequently used values yet"); + ImGui::TextDisabled("(Values used 3+ times will appear here)"); + } else { + for (auto* entry : mostUsedValues) { + std::string label = std::format("{}: {:.3f}##{}", entry->name, entry->value, (void*)entry); + if (ImGui::Selectable(label.c_str())) { + copiedValue = entry->value; + copiedValueName = entry->name; + hasValueInClipboard = true; + ImGui::SetClipboardText(std::to_string(entry->value).c_str()); + } + + // Right-click to remove + if (ImGui::BeginPopupContextItem()) { + if (ImGui::Selectable("Remove from palette")) { + auto it = std::find_if(valueEntries.begin(), valueEntries.end(), + [entry](const ValueEntry& e) { return &e == entry; }); + if (it != valueEntries.end()) { + valueEntries.erase(it); + Save(); + } + } + ImGui::EndPopup(); + } + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Used %d times\nClick to copy\nRight-click to remove", entry->useCount); + } + } + } + } + +std::vector PaletteWindow::GetRecentColors(int count) +{ + std::vector result; + std::vector allColors; + + for (auto& entry : colorEntries) { + allColors.push_back(&entry); + } + + std::sort(allColors.begin(), allColors.end(), [](ColorEntry* a, ColorEntry* b) { + return a->lastUsedTime > b->lastUsedTime; + }); + + for (int i = 0; i < std::min(count, (int)allColors.size()); i++) { + result.push_back(allColors[i]); + } + + return result; +} + +std::vector PaletteWindow::GetMostUsedColors(int count) +{ + std::vector result; + + for (auto& entry : colorEntries) { + if (entry.useCount >= 3 || entry.isFavorite) { + result.push_back(&entry); + } + } + + std::sort(result.begin(), result.end(), [](ColorEntry* a, ColorEntry* b) { + // Favorites always come first + if (a->isFavorite != b->isFavorite) + return a->isFavorite; + // Then sort by use count + return a->useCount > b->useCount; + }); + + if ((int)result.size() > count) { + result.resize(count); + } + + return result; +} + +std::vector PaletteWindow::GetRecentValues(int count) +{ + std::vector result; + std::vector allValues; + + for (auto& entry : valueEntries) { + allValues.push_back(&entry); + } + + std::sort(allValues.begin(), allValues.end(), [](ValueEntry* a, ValueEntry* b) { + return a->lastUsedTime > b->lastUsedTime; + }); + + for (int i = 0; i < std::min(count, (int)allValues.size()); i++) { + result.push_back(allValues[i]); + } + + return result; +} + +std::vector PaletteWindow::GetMostUsedValues(int count) +{ + std::vector result; + + for (auto& entry : valueEntries) { + if (entry.useCount >= 3 || entry.isFavorite) { + result.push_back(&entry); + } + } + + std::sort(result.begin(), result.end(), [](ValueEntry* a, ValueEntry* b) { + // Favorites always come first + if (a->isFavorite != b->isFavorite) + return a->isFavorite; + // Then sort by use count + return a->useCount > b->useCount; + }); + + if ((int)result.size() > count) { + result.resize(count); + } + + return result; +} + +void PaletteWindow::TrackColorUsage(const float3& color) +{ + float currentTime = static_cast(ImGui::GetTime()); + + // Find existing entry (with small epsilon for float comparison) + const float epsilon = 0.001f; + for (auto& entry : colorEntries) { + if (std::abs(entry.color.x - color.x) < epsilon && + std::abs(entry.color.y - color.y) < epsilon && + std::abs(entry.color.z - color.z) < epsilon) { + entry.useCount++; + entry.lastUsedTime = currentTime; + Save(); + return; + } + } + + // Add new entry + ColorEntry newEntry; + newEntry.color = color; + newEntry.useCount = 1; + newEntry.lastUsedTime = currentTime; + colorEntries.push_back(newEntry); + Save(); +} + +void PaletteWindow::TrackValueUsage(const std::string& name, float value) +{ + float currentTime = static_cast(ImGui::GetTime()); + + // Find existing entry + const float epsilon = 0.001f; + for (auto& entry : valueEntries) { + if (entry.name == name && std::abs(entry.value - value) < epsilon) { + entry.useCount++; + entry.lastUsedTime = currentTime; + Save(); + return; + } + } + + // Add new entry + ValueEntry newEntry; + newEntry.name = name; + newEntry.value = value; + newEntry.useCount = 1; + newEntry.lastUsedTime = currentTime; + valueEntries.push_back(newEntry); + Save(); +} + +void PaletteWindow::Save() +{ + auto editorWindow = EditorWindow::GetSingleton(); + + // Save favorites + editorWindow->settings.paletteFavorites = {}; + for (size_t i = 0; i < favoriteColors.size(); i++) { + if (favoriteColors[i].has_value()) { + editorWindow->settings.paletteFavorites[i].hasValue = true; + editorWindow->settings.paletteFavorites[i].r = favoriteColors[i].value().x; + editorWindow->settings.paletteFavorites[i].g = favoriteColors[i].value().y; + editorWindow->settings.paletteFavorites[i].b = favoriteColors[i].value().z; + } else { + editorWindow->settings.paletteFavorites[i].hasValue = false; + } + } + + // Save color entries + editorWindow->settings.paletteColors.clear(); + for (const auto& entry : colorEntries) { + EditorWindow::Settings::PaletteColorEntry e; + e.r = entry.color.x; + e.g = entry.color.y; + e.b = entry.color.z; + e.useCount = entry.useCount; + e.lastUsedTime = entry.lastUsedTime; + e.isFavorite = entry.isFavorite; + editorWindow->settings.paletteColors.push_back(e); + } + + // Save value entries + editorWindow->settings.paletteValues.clear(); + for (const auto& entry : valueEntries) { + EditorWindow::Settings::PaletteValueEntry e; + e.name = entry.name; + e.value = entry.value; + e.useCount = entry.useCount; + e.lastUsedTime = entry.lastUsedTime; + e.isFavorite = entry.isFavorite; + editorWindow->settings.paletteValues.push_back(e); + } + + editorWindow->Save(); +} + +void PaletteWindow::Load() +{ + auto editorWindow = EditorWindow::GetSingleton(); + + // Load favorites + for (size_t i = 0; i < editorWindow->settings.paletteFavorites.size(); i++) { + const auto& fav = editorWindow->settings.paletteFavorites[i]; + if (fav.hasValue) { + favoriteColors[i] = float3{ fav.r, fav.g, fav.b }; + } else { + favoriteColors[i].reset(); + } + } + + // Load color entries + colorEntries.clear(); + for (const auto& e : editorWindow->settings.paletteColors) { + ColorEntry entry; + entry.color = { e.r, e.g, e.b }; + entry.useCount = e.useCount; + entry.lastUsedTime = e.lastUsedTime; + entry.isFavorite = e.isFavorite; + colorEntries.push_back(entry); + } + + // Load value entries + valueEntries.clear(); + for (const auto& e : editorWindow->settings.paletteValues) { + ValueEntry entry; + entry.name = e.name; + entry.value = e.value; + entry.useCount = e.useCount; + entry.lastUsedTime = e.lastUsedTime; + entry.isFavorite = e.isFavorite; + valueEntries.push_back(entry); + } +} diff --git a/src/WeatherEditor/PaletteWindow.h b/src/WeatherEditor/PaletteWindow.h new file mode 100644 index 0000000000..bc7eb96a5c --- /dev/null +++ b/src/WeatherEditor/PaletteWindow.h @@ -0,0 +1,60 @@ +#pragma once + +#include "WeatherUtils.h" + +class PaletteWindow +{ +public: + static PaletteWindow* GetSingleton() + { + static PaletteWindow singleton; + return &singleton; + } + + bool open = true; + + struct ColorEntry + { + float3 color; + int useCount = 0; + float lastUsedTime = 0.0f; + bool isFavorite = false; + }; + + struct ValueEntry + { + std::string name; + float value; + int useCount = 0; + float lastUsedTime = 0.0f; + bool isFavorite = false; + }; + + void Draw(); + void TrackColorUsage(const float3& color); + void TrackValueUsage(const std::string& name, float value); + void Save(); + void Load(); + +private: + std::vector colorEntries; + std::vector valueEntries; + + // Favorites - fixed slots that users can drag colors into + static constexpr int maxFavoriteSlots = 10; + std::array, maxFavoriteSlots> favoriteColors; + + // Clipboard + float3 copiedColor = { 0.0f, 0.0f, 0.0f }; + float copiedValue = 0.0f; + std::string copiedValueName; + bool hasColorInClipboard = false; + bool hasValueInClipboard = false; + + void DrawColorsTab(); + void DrawValuesTab(); + std::vector GetRecentColors(int count); + std::vector GetMostUsedColors(int count); + std::vector GetRecentValues(int count); + std::vector GetMostUsedValues(int count); +}; diff --git a/src/WeatherEditor/Weather/CellLightingWidget.cpp b/src/WeatherEditor/Weather/CellLightingWidget.cpp new file mode 100644 index 0000000000..198fdc4cc0 --- /dev/null +++ b/src/WeatherEditor/Weather/CellLightingWidget.cpp @@ -0,0 +1,404 @@ +#include "CellLightingWidget.h" +#include "../EditorWindow.h" +#include "../WeatherUtils.h" + +void CellLightingWidget::DrawWidget() +{ + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { + DrawWidgetHeader("##CellLightingSearch", false, true); + + bool changed = false; + + if (!cell || !cell->IsInteriorCell()) { + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "This cell is not an interior cell."); + ImGui::TextWrapped("Cell lighting properties only apply to interior cells."); + ImGui::End(); + return; + } + + auto lighting = cell->GetLighting(); + if (!lighting) { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "No lighting data available for this cell."); + ImGui::End(); + return; + } + + if (ImGui::BeginTabBar("CellLightingTabs")) { + if (ImGui::BeginTabItem("Colors")) { + ImGui::SeparatorText("Ambient & Directional"); + if (WeatherUtils::DrawColorEdit("Ambient Color", settings.ambient)) changed = true; + if (WeatherUtils::DrawColorEdit("Directional Color", settings.directional)) changed = true; + if (WeatherUtils::DrawSliderFloat("Directional Fade", settings.directionalFade, 0.0f, 1.0f)) changed = true; + + ImGui::SeparatorText("Fog Colors"); + if (WeatherUtils::DrawColorEdit("Fog Near Color", settings.fogColorNear)) changed = true; + if (WeatherUtils::DrawColorEdit("Fog Far Color", settings.fogColorFar)) changed = true; + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Fog")) { + ImGui::SeparatorText("Fog Distance"); + if (WeatherUtils::DrawSliderFloat("Fog Near", settings.fogNear, 0.0f, 10000.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Fog Far", settings.fogFar, 0.0f, 50000.0f)) changed = true; + + ImGui::SeparatorText("Fog Properties"); + if (WeatherUtils::DrawSliderFloat("Fog Power", settings.fogPower, 0.0f, 10.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Fog Clamp (Max)", settings.fogClamp, 0.0f, 1.0f)) changed = true; + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Directional Ambient")) { + ImGui::SeparatorText("Directional Ambient Lighting (DALC)"); + + if (WeatherUtils::DrawColorEdit("X+ (Right)", settings.directionalXPlus)) changed = true; + if (WeatherUtils::DrawColorEdit("X- (Left)", settings.directionalXMinus)) changed = true; + if (WeatherUtils::DrawColorEdit("Y+ (Front)", settings.directionalYPlus)) changed = true; + if (WeatherUtils::DrawColorEdit("Y- (Back)", settings.directionalYMinus)) changed = true; + if (WeatherUtils::DrawColorEdit("Z+ (Up)", settings.directionalZPlus)) changed = true; + if (WeatherUtils::DrawColorEdit("Z- (Down)", settings.directionalZMinus)) changed = true; + if (WeatherUtils::DrawColorEdit("Specular", settings.directionalSpecular)) changed = true; + if (WeatherUtils::DrawSliderFloat("Fresnel Power", settings.fresnelPower, 0.0f, 10.0f)) changed = true; + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Advanced")) { + ImGui::SeparatorText("Light Fade Distances"); + if (WeatherUtils::DrawSliderFloat("Light Fade Start", settings.lightFadeStart, 0.0f, 10000.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Light Fade End", settings.lightFadeEnd, 0.0f, 20000.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Clip Distance", settings.clipDist, 0.0f, 50000.0f)) changed = true; + + ImGui::SeparatorText("Directional Rotation"); + int xyDegrees = settings.directionalXY; + int zDegrees = settings.directionalZ; + if (ImGui::SliderInt("XY Rotation", &xyDegrees, 0, 360)) { + settings.directionalXY = static_cast(xyDegrees); + changed = true; + } + if (ImGui::SliderInt("Z Rotation", &zDegrees, 0, 360)) { + settings.directionalZ = static_cast(zDegrees); + changed = true; + } + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Inheritance")) { + ImGui::TextWrapped("These flags control which lighting properties are inherited from the cell's lighting template."); + ImGui::Separator(); + + if (ImGui::Checkbox("Inherit Ambient Color", &settings.inheritAmbientColor)) changed = true; + if (ImGui::Checkbox("Inherit Directional Color", &settings.inheritDirectionalColor)) changed = true; + if (ImGui::Checkbox("Inherit Fog Color", &settings.inheritFogColor)) changed = true; + if (ImGui::Checkbox("Inherit Fog Near", &settings.inheritFogNear)) changed = true; + if (ImGui::Checkbox("Inherit Fog Far", &settings.inheritFogFar)) changed = true; + if (ImGui::Checkbox("Inherit Directional Rotation", &settings.inheritDirectionalRotation)) changed = true; + if (ImGui::Checkbox("Inherit Directional Fade", &settings.inheritDirectionalFade)) changed = true; + if (ImGui::Checkbox("Inherit Clip Distance", &settings.inheritClipDistance)) changed = true; + if (ImGui::Checkbox("Inherit Fog Power", &settings.inheritFogPower)) changed = true; + if (ImGui::Checkbox("Inherit Fog Max (Clamp)", &settings.inheritFogMax)) changed = true; + if (ImGui::Checkbox("Inherit Light Fade Distances", &settings.inheritLightFadeDistances)) changed = true; + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + + ImGui::End(); + } +} + +void CellLightingWidget::LoadSettings() +{ + if (!cell || !cell->IsInteriorCell()) + return; + + auto lighting = cell->GetLighting(); + if (!lighting) + return; + + // Try to load from JSON first + if (!js.empty()) { + try { + if (js.contains("ambient")) { + auto arr = js["ambient"]; + if (arr.is_array() && arr.size() == 3) { + settings.ambient = { arr[0], arr[1], arr[2] }; + } + } + if (js.contains("directional")) { + auto arr = js["directional"]; + if (arr.is_array() && arr.size() == 3) { + settings.directional = { arr[0], arr[1], arr[2] }; + } + } + if (js.contains("fogColorNear")) { + auto arr = js["fogColorNear"]; + if (arr.is_array() && arr.size() == 3) { + settings.fogColorNear = { arr[0], arr[1], arr[2] }; + } + } + if (js.contains("fogColorFar")) { + auto arr = js["fogColorFar"]; + if (arr.is_array() && arr.size() == 3) { + settings.fogColorFar = { arr[0], arr[1], arr[2] }; + } + } + if (js.contains("fogNear")) settings.fogNear = js["fogNear"]; + if (js.contains("fogFar")) settings.fogFar = js["fogFar"]; + if (js.contains("fogPower")) settings.fogPower = js["fogPower"]; + if (js.contains("fogClamp")) settings.fogClamp = js["fogClamp"]; + if (js.contains("directionalFade")) settings.directionalFade = js["directionalFade"]; + if (js.contains("clipDist")) settings.clipDist = js["clipDist"]; + if (js.contains("lightFadeStart")) settings.lightFadeStart = js["lightFadeStart"]; + if (js.contains("lightFadeEnd")) settings.lightFadeEnd = js["lightFadeEnd"]; + if (js.contains("directionalXY")) settings.directionalXY = js["directionalXY"]; + if (js.contains("directionalZ")) settings.directionalZ = js["directionalZ"]; + + if (js.contains("dalc")) { + auto& dalc = js["dalc"]; + if (dalc.contains("xPlus") && dalc["xPlus"].is_array() && dalc["xPlus"].size() == 3) { + settings.directionalXPlus = { dalc["xPlus"][0], dalc["xPlus"][1], dalc["xPlus"][2] }; + } + if (dalc.contains("xMinus") && dalc["xMinus"].is_array() && dalc["xMinus"].size() == 3) { + settings.directionalXMinus = { dalc["xMinus"][0], dalc["xMinus"][1], dalc["xMinus"][2] }; + } + if (dalc.contains("yPlus") && dalc["yPlus"].is_array() && dalc["yPlus"].size() == 3) { + settings.directionalYPlus = { dalc["yPlus"][0], dalc["yPlus"][1], dalc["yPlus"][2] }; + } + if (dalc.contains("yMinus") && dalc["yMinus"].is_array() && dalc["yMinus"].size() == 3) { + settings.directionalYMinus = { dalc["yMinus"][0], dalc["yMinus"][1], dalc["yMinus"][2] }; + } + if (dalc.contains("zPlus") && dalc["zPlus"].is_array() && dalc["zPlus"].size() == 3) { + settings.directionalZPlus = { dalc["zPlus"][0], dalc["zPlus"][1], dalc["zPlus"][2] }; + } + if (dalc.contains("zMinus") && dalc["zMinus"].is_array() && dalc["zMinus"].size() == 3) { + settings.directionalZMinus = { dalc["zMinus"][0], dalc["zMinus"][1], dalc["zMinus"][2] }; + } + if (dalc.contains("specular") && dalc["specular"].is_array() && dalc["specular"].size() == 3) { + settings.directionalSpecular = { dalc["specular"][0], dalc["specular"][1], dalc["specular"][2] }; + } + if (dalc.contains("fresnelPower")) settings.fresnelPower = dalc["fresnelPower"]; + } + + if (js.contains("inherit")) { + auto& inherit = js["inherit"]; + if (inherit.contains("ambientColor")) settings.inheritAmbientColor = inherit["ambientColor"]; + if (inherit.contains("directionalColor")) settings.inheritDirectionalColor = inherit["directionalColor"]; + if (inherit.contains("fogColor")) settings.inheritFogColor = inherit["fogColor"]; + if (inherit.contains("fogNear")) settings.inheritFogNear = inherit["fogNear"]; + if (inherit.contains("fogFar")) settings.inheritFogFar = inherit["fogFar"]; + if (inherit.contains("directionalRotation")) settings.inheritDirectionalRotation = inherit["directionalRotation"]; + if (inherit.contains("directionalFade")) settings.inheritDirectionalFade = inherit["directionalFade"]; + if (inherit.contains("clipDistance")) settings.inheritClipDistance = inherit["clipDistance"]; + if (inherit.contains("fogPower")) settings.inheritFogPower = inherit["fogPower"]; + if (inherit.contains("fogMax")) settings.inheritFogMax = inherit["fogMax"]; + if (inherit.contains("lightFadeDistances")) settings.inheritLightFadeDistances = inherit["lightFadeDistances"]; + } + + } catch (const std::exception& e) { + logger::error("CellLighting {}: Failed to load from JSON: {}", GetEditorID(), e.what()); + // Fall through to load from form + } + } else { + // No JSON data, load from game form + ColorToFloat3(lighting->ambient, settings.ambient); + ColorToFloat3(lighting->directional, settings.directional); + ColorToFloat3(lighting->fogColorNear, settings.fogColorNear); + ColorToFloat3(lighting->fogColorFar, settings.fogColorFar); + + settings.fogNear = lighting->fogNear; + settings.fogFar = lighting->fogFar; + settings.fogPower = lighting->fogPower; + settings.fogClamp = lighting->fogClamp; + + settings.directionalFade = lighting->directionalFade; + settings.clipDist = lighting->clipDist; + settings.lightFadeStart = lighting->lightFadeStart; + settings.lightFadeEnd = lighting->lightFadeEnd; + settings.directionalXY = lighting->directionalXY; + settings.directionalZ = lighting->directionalZ; + + auto& dalc = lighting->directionalAmbientLightingColors; + ColorToFloat3(dalc.directional.x.max, settings.directionalXPlus); + ColorToFloat3(dalc.directional.x.min, settings.directionalXMinus); + ColorToFloat3(dalc.directional.y.max, settings.directionalYPlus); + ColorToFloat3(dalc.directional.y.min, settings.directionalYMinus); + ColorToFloat3(dalc.directional.z.max, settings.directionalZPlus); + ColorToFloat3(dalc.directional.z.min, settings.directionalZMinus); + ColorToFloat3(dalc.specular, settings.directionalSpecular); + settings.fresnelPower = dalc.fresnelPower; + + auto flags = lighting->lightingTemplateInheritanceFlags; + settings.inheritAmbientColor = flags.any(RE::INTERIOR_DATA::Inherit::kAmbientColor); + settings.inheritDirectionalColor = flags.any(RE::INTERIOR_DATA::Inherit::kDirectionalColor); + settings.inheritFogColor = flags.any(RE::INTERIOR_DATA::Inherit::kFogColor); + settings.inheritFogNear = flags.any(RE::INTERIOR_DATA::Inherit::kFogNear); + settings.inheritFogFar = flags.any(RE::INTERIOR_DATA::Inherit::kFogFar); + settings.inheritDirectionalRotation = flags.any(RE::INTERIOR_DATA::Inherit::kDirectionalRotation); + settings.inheritDirectionalFade = flags.any(RE::INTERIOR_DATA::Inherit::kDirectionalFade); + settings.inheritClipDistance = flags.any(RE::INTERIOR_DATA::Inherit::kClipDistance); + settings.inheritFogPower = flags.any(RE::INTERIOR_DATA::Inherit::kFogPower); + settings.inheritFogMax = flags.any(RE::INTERIOR_DATA::Inherit::kFogMax); + settings.inheritLightFadeDistances = flags.any(RE::INTERIOR_DATA::Inherit::kLightFadeDistances); + } + + originalSettings = settings; +} + +void CellLightingWidget::SaveSettings() +{ + js["ambient"] = { settings.ambient.x, settings.ambient.y, settings.ambient.z }; + js["directional"] = { settings.directional.x, settings.directional.y, settings.directional.z }; + js["fogColorNear"] = { settings.fogColorNear.x, settings.fogColorNear.y, settings.fogColorNear.z }; + js["fogColorFar"] = { settings.fogColorFar.x, settings.fogColorFar.y, settings.fogColorFar.z }; + js["fogNear"] = settings.fogNear; + js["fogFar"] = settings.fogFar; + js["fogPower"] = settings.fogPower; + js["fogClamp"] = settings.fogClamp; + js["directionalFade"] = settings.directionalFade; + js["clipDist"] = settings.clipDist; + js["lightFadeStart"] = settings.lightFadeStart; + js["lightFadeEnd"] = settings.lightFadeEnd; + js["directionalXY"] = settings.directionalXY; + js["directionalZ"] = settings.directionalZ; + + js["dalc"]["xPlus"] = { settings.directionalXPlus.x, settings.directionalXPlus.y, settings.directionalXPlus.z }; + js["dalc"]["xMinus"] = { settings.directionalXMinus.x, settings.directionalXMinus.y, settings.directionalXMinus.z }; + js["dalc"]["yPlus"] = { settings.directionalYPlus.x, settings.directionalYPlus.y, settings.directionalYPlus.z }; + js["dalc"]["yMinus"] = { settings.directionalYMinus.x, settings.directionalYMinus.y, settings.directionalYMinus.z }; + js["dalc"]["zPlus"] = { settings.directionalZPlus.x, settings.directionalZPlus.y, settings.directionalZPlus.z }; + js["dalc"]["zMinus"] = { settings.directionalZMinus.x, settings.directionalZMinus.y, settings.directionalZMinus.z }; + js["dalc"]["specular"] = { settings.directionalSpecular.x, settings.directionalSpecular.y, settings.directionalSpecular.z }; + js["dalc"]["fresnelPower"] = settings.fresnelPower; + + js["inherit"]["ambientColor"] = settings.inheritAmbientColor; + js["inherit"]["directionalColor"] = settings.inheritDirectionalColor; + js["inherit"]["fogColor"] = settings.inheritFogColor; + js["inherit"]["fogNear"] = settings.inheritFogNear; + js["inherit"]["fogFar"] = settings.inheritFogFar; + js["inherit"]["directionalRotation"] = settings.inheritDirectionalRotation; + js["inherit"]["directionalFade"] = settings.inheritDirectionalFade; + js["inherit"]["clipDistance"] = settings.inheritClipDistance; + js["inherit"]["fogPower"] = settings.inheritFogPower; + js["inherit"]["fogMax"] = settings.inheritFogMax; + js["inherit"]["lightFadeDistances"] = settings.inheritLightFadeDistances; +} + +void CellLightingWidget::ApplyChanges() +{ + if (!cell || !cell->IsInteriorCell()) + return; + + auto lighting = cell->GetLighting(); + if (!lighting) + return; + + // Apply basic colors + Float3ToColor(settings.ambient, lighting->ambient); + Float3ToColor(settings.directional, lighting->directional); + Float3ToColor(settings.fogColorNear, lighting->fogColorNear); + Float3ToColor(settings.fogColorFar, lighting->fogColorFar); + + // Apply fog properties + lighting->fogNear = settings.fogNear; + lighting->fogFar = settings.fogFar; + lighting->fogPower = settings.fogPower; + lighting->fogClamp = settings.fogClamp; + + // Apply advanced properties + lighting->directionalFade = settings.directionalFade; + lighting->clipDist = settings.clipDist; + lighting->lightFadeStart = settings.lightFadeStart; + lighting->lightFadeEnd = settings.lightFadeEnd; + lighting->directionalXY = settings.directionalXY; + lighting->directionalZ = settings.directionalZ; + + // Apply directional ambient lighting colors + auto& dalc = lighting->directionalAmbientLightingColors; + Float3ToColor(settings.directionalXPlus, dalc.directional.x.max); + Float3ToColor(settings.directionalXMinus, dalc.directional.x.min); + Float3ToColor(settings.directionalYPlus, dalc.directional.y.max); + Float3ToColor(settings.directionalYMinus, dalc.directional.y.min); + Float3ToColor(settings.directionalZPlus, dalc.directional.z.max); + Float3ToColor(settings.directionalZMinus, dalc.directional.z.min); + Float3ToColor(settings.directionalSpecular, dalc.specular); + dalc.fresnelPower = settings.fresnelPower; + + // Apply inheritance flags + lighting->lightingTemplateInheritanceFlags.reset(); + if (settings.inheritAmbientColor) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kAmbientColor); + if (settings.inheritDirectionalColor) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kDirectionalColor); + if (settings.inheritFogColor) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kFogColor); + if (settings.inheritFogNear) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kFogNear); + if (settings.inheritFogFar) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kFogFar); + if (settings.inheritDirectionalRotation) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kDirectionalRotation); + if (settings.inheritDirectionalFade) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kDirectionalFade); + if (settings.inheritClipDistance) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kClipDistance); + if (settings.inheritFogPower) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kFogPower); + if (settings.inheritFogMax) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kFogMax); + if (settings.inheritLightFadeDistances) + lighting->lightingTemplateInheritanceFlags.set(RE::INTERIOR_DATA::Inherit::kLightFadeDistances); + + originalSettings = settings; +} + +void CellLightingWidget::RevertChanges() +{ + settings = originalSettings; +} + +bool CellLightingWidget::HasUnsavedChanges() const +{ + return settings.ambient != originalSettings.ambient || + settings.directional != originalSettings.directional || + settings.fogColorNear != originalSettings.fogColorNear || + settings.fogColorFar != originalSettings.fogColorFar || + settings.fogNear != originalSettings.fogNear || + settings.fogFar != originalSettings.fogFar || + settings.fogPower != originalSettings.fogPower || + settings.fogClamp != originalSettings.fogClamp || + settings.directionalFade != originalSettings.directionalFade || + settings.clipDist != originalSettings.clipDist || + settings.lightFadeStart != originalSettings.lightFadeStart || + settings.lightFadeEnd != originalSettings.lightFadeEnd || + settings.directionalXY != originalSettings.directionalXY || + settings.directionalZ != originalSettings.directionalZ || + settings.directionalXPlus != originalSettings.directionalXPlus || + settings.directionalXMinus != originalSettings.directionalXMinus || + settings.directionalYPlus != originalSettings.directionalYPlus || + settings.directionalYMinus != originalSettings.directionalYMinus || + settings.directionalZPlus != originalSettings.directionalZPlus || + settings.directionalZMinus != originalSettings.directionalZMinus || + settings.directionalSpecular != originalSettings.directionalSpecular || + settings.fresnelPower != originalSettings.fresnelPower || + settings.inheritAmbientColor != originalSettings.inheritAmbientColor || + settings.inheritDirectionalColor != originalSettings.inheritDirectionalColor || + settings.inheritFogColor != originalSettings.inheritFogColor || + settings.inheritFogNear != originalSettings.inheritFogNear || + settings.inheritFogFar != originalSettings.inheritFogFar || + settings.inheritDirectionalRotation != originalSettings.inheritDirectionalRotation || + settings.inheritDirectionalFade != originalSettings.inheritDirectionalFade || + settings.inheritClipDistance != originalSettings.inheritClipDistance || + settings.inheritFogPower != originalSettings.inheritFogPower || + settings.inheritFogMax != originalSettings.inheritFogMax || + settings.inheritLightFadeDistances != originalSettings.inheritLightFadeDistances; +} diff --git a/src/WeatherEditor/Weather/CellLightingWidget.h b/src/WeatherEditor/Weather/CellLightingWidget.h new file mode 100644 index 0000000000..567049c539 --- /dev/null +++ b/src/WeatherEditor/Weather/CellLightingWidget.h @@ -0,0 +1,70 @@ +#pragma once + +#include "../Widget.h" + +class CellLightingWidget : public Widget +{ +public: + CellLightingWidget(RE::TESObjectCELL* a_cell) : + cell(a_cell) + { + form = a_cell; + } + + ~CellLightingWidget() override = default; + + void DrawWidget() override; + void LoadSettings() override; + void SaveSettings() override; + void ApplyChanges() override; + void RevertChanges() override; + bool HasUnsavedChanges() const override; + + RE::TESObjectCELL* cell = nullptr; + +private: + struct Settings + { + // INTERIOR_DATA properties + float3 ambient = { 1.0f, 1.0f, 1.0f }; + float3 directional = { 1.0f, 1.0f, 1.0f }; + float3 fogColorNear = { 1.0f, 1.0f, 1.0f }; + float3 fogColorFar = { 1.0f, 1.0f, 1.0f }; + float fogNear = 0.0f; + float fogFar = 10000.0f; + float fogPower = 1.0f; + float fogClamp = 1.0f; + float directionalFade = 1.0f; + float clipDist = 10000.0f; + float lightFadeStart = 3500.0f; + float lightFadeEnd = 5000.0f; + uint32_t directionalXY = 0; + uint32_t directionalZ = 0; + + // Directional ambient lighting colors (DALC equivalent) + float3 directionalXPlus = { 1.0f, 1.0f, 1.0f }; + float3 directionalXMinus = { 1.0f, 1.0f, 1.0f }; + float3 directionalYPlus = { 1.0f, 1.0f, 1.0f }; + float3 directionalYMinus = { 1.0f, 1.0f, 1.0f }; + float3 directionalZPlus = { 1.0f, 1.0f, 1.0f }; + float3 directionalZMinus = { 1.0f, 1.0f, 1.0f }; + float3 directionalSpecular = { 1.0f, 1.0f, 1.0f }; + float fresnelPower = 1.0f; + + // Inheritance flags + bool inheritAmbientColor = false; + bool inheritDirectionalColor = false; + bool inheritFogColor = false; + bool inheritFogNear = false; + bool inheritFogFar = false; + bool inheritDirectionalRotation = false; + bool inheritDirectionalFade = false; + bool inheritClipDistance = false; + bool inheritFogPower = false; + bool inheritFogMax = false; + bool inheritLightFadeDistances = false; + }; + + Settings settings; + Settings originalSettings; +}; diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp index e8e960d4f9..0ef4a82420 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -29,19 +29,13 @@ ImageSpaceWidget::~ImageSpaceWidget() void ImageSpaceWidget::DrawWidget() { - if (!open) - return; - auto editorWindow = EditorWindow::GetSingleton(); - ImGui::SetNextWindowSize(ImVec2(600, 800), ImGuiCond_FirstUseEver); - if (ImGui::Begin(std::format("ImageSpace Editor - {}", GetEditorID()).c_str(), &open)) { - ImGui::Text("Form ID: %s", GetFormID().c_str()); - ImGui::Text("Plugin: %s", GetFilename().c_str()); - ImGui::Separator(); - - // Search bar (activatable with Ctrl+F) - BeginWidgetSearchBar(searchBuffer, sizeof(searchBuffer), searchActive); + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { + + // Draw header with search and Save/Load/Delete buttons + DrawWidgetHeader("##ImageSpaceSearch", false, true); // Draw all settings in a unified table if (ImGui::BeginTable("ImageSpaceSettings", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { @@ -221,74 +215,6 @@ void ImageSpaceWidget::DrawWidget() ApplyChanges(); } } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - // Action buttons - ImGui::BeginDisabled(!editorWindow->settings.autoApplyChanges); - if (ImGui::Button("Apply", ImVec2(-1, 0))) { - ApplyChanges(); - } - ImGui::EndDisabled(); - - if (ImGui::Button("Revert", ImVec2(-1, 0))) { - RevertChanges(); - } - - // Save/Load buttons (always visible) - if (ImGui::Button("Save to File", ImVec2(-1, 0))) { - Save(); - } - - if (ImGui::Button("Load from File", ImVec2(-1, 0))) { - Load(); - } - - // Delete button with confirmation (only show if file exists) - std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName(), GetEditorID()); - if (std::filesystem::exists(filePath)) { - static bool showDeleteConfirm = false; - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); - if (ImGui::ImageButton("##DeleteImageSpace", globals::menu->uiIcons.deleteSettings.texture, ImVec2(32, 32))) { - if (editorWindow->settings.suppressDeleteWarning) { - Delete(); - } else { - showDeleteConfirm = true; - } - } - ImGui::PopStyleColor(2); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Delete saved file and revert to defaults"); - } - - if (showDeleteConfirm) { - ImGui::OpenPopup("Delete Confirmation##ImageSpace"); - } - - if (ImGui::BeginPopupModal("Delete Confirmation##ImageSpace", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Are you sure you want to delete the settings file?"); - ImGui::Text("This will reload default values from the game."); - ImGui::Spacing(); - - ImGui::Checkbox("Don't ask again", &editorWindow->settings.suppressDeleteWarning); - - ImGui::Spacing(); - if (ImGui::Button("Delete", ImVec2(120, 0))) { - Delete(); - showDeleteConfirm = false; - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) { - showDeleteConfirm = false; - ImGui::CloseCurrentPopup(); - } - ImGui::EndPopup(); - } - } } ImGui::End(); } @@ -296,55 +222,20 @@ void ImageSpaceWidget::DrawWidget() void ImageSpaceWidget::LoadSettings() { try { - if (!js.empty() && js.contains("Settings")) { - auto settingsJson = js["Settings"]; - - // Validate that we have actual data - bool hasValidData = false; - for (const auto& [key, value] : settingsJson.items()) { - if (value.is_number() && value.get() != 0.0f) { - hasValidData = true; - break; - } - if (value.is_array() && !value.empty()) { - hasValidData = true; - break; - } - } - - if (hasValidData) { - settings = settingsJson; - logger::info("ImageSpace settings loaded successfully for {}", GetEditorID()); - } else { - logger::warn("ImageSpace settings contained only zero values for {}, loading from form", GetEditorID()); - LoadImageSpaceValues(); - EditorWindow::GetSingleton()->ShowNotification( - std::format("Failed to load {} - using default values", GetEditorID()), - ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); - } + if (!js.empty() && js.contains("Settings") && js["Settings"].is_object()) { + settings = js["Settings"]; } else { LoadImageSpaceValues(); } } catch (const std::exception& e) { logger::error("Failed to load ImageSpace settings for {}: {}", GetEditorID(), e.what()); LoadImageSpaceValues(); - EditorWindow::GetSingleton()->ShowNotification( - std::format("Error loading {} - using default values", GetEditorID()), - ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); } } void ImageSpaceWidget::SaveSettings() { - try { - js["Settings"] = settings; - logger::info("ImageSpace settings saved for {}", GetEditorID()); - } catch (const std::exception& e) { - logger::error("Failed to save ImageSpace settings for {}: {}", GetEditorID(), e.what()); - EditorWindow::GetSingleton()->ShowNotification( - std::format("Failed to save {}", GetEditorID()), - ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); - } + js["Settings"] = settings; } void ImageSpaceWidget::SetImageSpaceValues() diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.h b/src/WeatherEditor/Weather/ImageSpaceWidget.h index 0a718dacca..68b27796e3 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.h +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.h @@ -9,6 +9,10 @@ class ImageSpaceWidget : public Widget ImageSpaceWidget(RE::TESImageSpace* a_imageSpace) { + if (!a_imageSpace) { + logger::error("ImageSpaceWidget created with null pointer"); + return; + } form = a_imageSpace; imageSpace = a_imageSpace; LoadImageSpaceValues(); diff --git a/src/WeatherEditor/Weather/LensFlareWidget.cpp b/src/WeatherEditor/Weather/LensFlareWidget.cpp new file mode 100644 index 0000000000..284dc8c805 --- /dev/null +++ b/src/WeatherEditor/Weather/LensFlareWidget.cpp @@ -0,0 +1,70 @@ +#include "LensFlareWidget.h" +#include "../EditorWindow.h" +#include "../WeatherUtils.h" + +void LensFlareWidget::DrawWidget() +{ + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { + DrawWidgetHeader("##LensFlareSearch", false, true); + + bool changed = false; + + ImGui::SeparatorText("Fade Distance"); + if (ImGui::SliderFloat("Fade Dist Radius Scale", &settings.fadeDistRadiusScale, 0.0f, 10.0f)) changed = true; + + ImGui::SeparatorText("Color"); + if (ImGui::SliderFloat("Color Influence", &settings.colorInfluence, 0.0f, 1.0f)) changed = true; + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } + ImGui::End(); +} + +void LensFlareWidget::LoadSettings() +{ + if (!lensFlare) + return; + + if (!js.empty()) { + try { + if (js.contains("fadeDistRadiusScale")) settings.fadeDistRadiusScale = js["fadeDistRadiusScale"]; + if (js.contains("colorInfluence")) settings.colorInfluence = js["colorInfluence"]; + } catch (const std::exception& e) { + logger::error("LensFlare {}: Failed to load from JSON: {}", GetEditorID(), e.what()); + } +} else { + settings.fadeDistRadiusScale = lensFlare->fadeDistRadiusScale; + settings.colorInfluence = lensFlare->colorInfluence; +} originalSettings = settings; +} + +void LensFlareWidget::SaveSettings() +{ + js["fadeDistRadiusScale"] = settings.fadeDistRadiusScale; + js["colorInfluence"] = settings.colorInfluence; +} + +void LensFlareWidget::ApplyChanges() +{ + if (!lensFlare) + return; + + lensFlare->fadeDistRadiusScale = settings.fadeDistRadiusScale; + lensFlare->colorInfluence = settings.colorInfluence; + + originalSettings = settings; +} + +void LensFlareWidget::RevertChanges() +{ + settings = originalSettings; +} + +bool LensFlareWidget::HasUnsavedChanges() const +{ + return settings.fadeDistRadiusScale != originalSettings.fadeDistRadiusScale || + settings.colorInfluence != originalSettings.colorInfluence; +} diff --git a/src/WeatherEditor/Weather/LensFlareWidget.h b/src/WeatherEditor/Weather/LensFlareWidget.h new file mode 100644 index 0000000000..56979d9dd1 --- /dev/null +++ b/src/WeatherEditor/Weather/LensFlareWidget.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../Widget.h" + +class LensFlareWidget : public Widget +{ +public: + LensFlareWidget(RE::BGSLensFlare* a_lensFlare) : + lensFlare(a_lensFlare) + { + form = a_lensFlare; + } + + ~LensFlareWidget() override = default; + + void DrawWidget() override; + void LoadSettings() override; + void SaveSettings() override; + void ApplyChanges() override; + void RevertChanges() override; + bool HasUnsavedChanges() const override; + + RE::BGSLensFlare* lensFlare = nullptr; + +private: + struct Settings + { + float fadeDistRadiusScale = 1.0f; + float colorInfluence = 0.2f; + }; + + Settings settings; + Settings originalSettings; +}; diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index a3efb7e0af..ae04d4ac4b 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -62,33 +62,33 @@ void LightingTemplateWidget::DrawBasicSettings() if (ImGui::CollapsingHeader("Ambient & Directional", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Ambient Color") && DrawColorEdit("Ambient Color", settings.ambient)) changed = true; + if (MatchesSearch("Ambient Color") && WeatherUtils::DrawColorEdit("Ambient Color", settings.ambient)) changed = true; if (MatchesSearch("Ambient Color")) ImGui::Spacing(); - if (MatchesSearch("Directional Color") && DrawColorEdit("Directional Color", settings.directional)) changed = true; + if (MatchesSearch("Directional Color") && WeatherUtils::DrawColorEdit("Directional Color", settings.directional)) changed = true; if (MatchesSearch("Directional Color")) ImGui::Spacing(); } if (ImGui::CollapsingHeader("Directional Settings", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Directional XY") && DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; + if (MatchesSearch("Directional XY") && WeatherUtils::DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; if (MatchesSearch("Directional XY")) ImGui::Spacing(); - if (MatchesSearch("Directional Z") && DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; + if (MatchesSearch("Directional Z") && WeatherUtils::DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; if (MatchesSearch("Directional Z")) ImGui::Spacing(); - if (MatchesSearch("Directional Fade") && DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; + if (MatchesSearch("Directional Fade") && WeatherUtils::DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; if (MatchesSearch("Directional Fade")) ImGui::Spacing(); } if (ImGui::CollapsingHeader("Light Fade", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Light Fade Start") && DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; + if (MatchesSearch("Light Fade Start") && WeatherUtils::DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; if (MatchesSearch("Light Fade Start")) ImGui::Spacing(); - if (MatchesSearch("Light Fade End") && DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; + if (MatchesSearch("Light Fade End") && WeatherUtils::DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; if (MatchesSearch("Light Fade End")) ImGui::Spacing(); } if (ImGui::CollapsingHeader("Other", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Clip Distance") && DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; + if (MatchesSearch("Clip Distance") && WeatherUtils::DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; if (MatchesSearch("Clip Distance")) ImGui::Spacing(); } @@ -102,21 +102,21 @@ void LightingTemplateWidget::DrawFogSettings() bool changed = false; ImGui::Spacing(); - if (MatchesSearch("Fog Color Near") && DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; + if (MatchesSearch("Fog Color Near") && WeatherUtils::DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; if (MatchesSearch("Fog Color Near")) ImGui::Spacing(); - if (MatchesSearch("Fog Color Far") && DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; + if (MatchesSearch("Fog Color Far") && WeatherUtils::DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; if (MatchesSearch("Fog Color Far")) ImGui::Spacing(); ImGui::Spacing(); - if (MatchesSearch("Fog Near") && DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; + if (MatchesSearch("Fog Near") && WeatherUtils::DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; if (MatchesSearch("Fog Near")) ImGui::Spacing(); - if (MatchesSearch("Fog Far") && DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; + if (MatchesSearch("Fog Far") && WeatherUtils::DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; if (MatchesSearch("Fog Far")) ImGui::Spacing(); ImGui::Spacing(); - if (MatchesSearch("Fog Power") && DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; + if (MatchesSearch("Fog Power") && WeatherUtils::DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; if (MatchesSearch("Fog Power")) ImGui::Spacing(); - if (MatchesSearch("Fog Clamp") && DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; + if (MatchesSearch("Fog Clamp") && WeatherUtils::DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; if (MatchesSearch("Fog Clamp")) ImGui::Spacing(); if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { @@ -130,9 +130,9 @@ void LightingTemplateWidget::DrawDALCSettings() if (ImGui::CollapsingHeader("Basic DALC", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (DrawColorEdit("Specular", settings.dalc.specular)) changed = true; + if (WeatherUtils::DrawColorEdit("Specular", settings.dalc.specular)) changed = true; ImGui::Spacing(); - if (DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; + if (WeatherUtils::DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; ImGui::Spacing(); } @@ -166,14 +166,14 @@ void LightingTemplateWidget::DrawDALCSettings() maxColors[TOD::Night] = settings.dalc.directional[2].max; minColors[TOD::Night] = settings.dalc.directional[2].min; - if (TOD::DrawTODColorRow("Max", maxColors)) { + if (TOD::DrawTODColorRow("Positive Direction (+)", maxColors)) { settings.dalc.directional[0].max = maxColors[TOD::Sunrise]; // X from Sunrise settings.dalc.directional[1].max = maxColors[TOD::Sunset]; // Y from Sunset settings.dalc.directional[2].max = maxColors[TOD::Night]; // Z from Night changed = true; } - if (TOD::DrawTODColorRow("Min", minColors)) { + if (TOD::DrawTODColorRow("Negative Direction (-)", minColors)) { settings.dalc.directional[0].min = minColors[TOD::Sunrise]; // X from Sunrise settings.dalc.directional[1].min = minColors[TOD::Sunset]; // Y from Sunset settings.dalc.directional[2].min = minColors[TOD::Night]; // Z from Night @@ -236,6 +236,9 @@ void LightingTemplateWidget::SetLightingTemplateValues() void LightingTemplateWidget::LoadLightingTemplateValues() { + if (!lightingTemplate) + return; + auto& data = lightingTemplate->data; auto& dalc = lightingTemplate->directionalAmbientLightingColors; diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.h b/src/WeatherEditor/Weather/LightingTemplateWidget.h index d9e5627701..f179b806db 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.h +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.h @@ -9,6 +9,10 @@ class LightingTemplateWidget : public Widget LightingTemplateWidget(RE::BGSLightingTemplate* a_lightingTemplate) { + if (!a_lightingTemplate) { + logger::error("LightingTemplateWidget created with null pointer"); + return; + } form = a_lightingTemplate; lightingTemplate = a_lightingTemplate; LoadLightingTemplateValues(); diff --git a/src/WeatherEditor/Weather/PrecipitationWidget.cpp b/src/WeatherEditor/Weather/PrecipitationWidget.cpp new file mode 100644 index 0000000000..7b0ca2ae2f --- /dev/null +++ b/src/WeatherEditor/Weather/PrecipitationWidget.cpp @@ -0,0 +1,186 @@ +#include "PrecipitationWidget.h" +#include "../EditorWindow.h" +#include "../WeatherUtils.h" + +void PrecipitationWidget::DrawWidget() +{ + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { + DrawWidgetHeader("##PrecipitationSearch", false, true); + + bool changed = false; + + if (ImGui::BeginTabBar("PrecipitationTabs")) { + if (ImGui::BeginTabItem("Particle")) { + ImGui::SeparatorText("Particle Type"); + const char* types[] = { "Rain", "Snow" }; + int currentType = static_cast(settings.particleType); + if (ImGui::Combo("Type", ¤tType, types, IM_ARRAYSIZE(types))) { + settings.particleType = static_cast(currentType); + changed = true; + } + + ImGui::SeparatorText("Particle Size"); + if (WeatherUtils::DrawSliderFloat("Size X", settings.particleSizeX, 0.0f, 10.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Size Y", settings.particleSizeY, 0.0f, 10.0f)) changed = true; + + ImGui::SeparatorText("Velocity"); + if (WeatherUtils::DrawSliderFloat("Gravity Velocity", settings.gravityVelocity, -100.0f, 100.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Rotation Velocity", settings.rotationVelocity, -360.0f, 360.0f)) changed = true; + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Position")) { + ImGui::SeparatorText("Offset"); + if (WeatherUtils::DrawSliderFloat("Center Offset Min", settings.centerOffsetMin, -1000.0f, 1000.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Center Offset Max", settings.centerOffsetMax, -1000.0f, 1000.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Start Rotation Range", settings.startRotationRange, 0.0f, 360.0f)) changed = true; + + ImGui::SeparatorText("Volume"); + if (WeatherUtils::DrawSliderFloat("Box Size", settings.boxSize, 0.0f, 10000.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Particle Density", settings.particleDensity, 0.0f, 10.0f)) changed = true; + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Texture")) { + ImGui::SeparatorText("Subtextures"); + int numX = static_cast(settings.numSubtexturesX); + int numY = static_cast(settings.numSubtexturesY); + if (ImGui::InputInt("Num Subtextures X", &numX)) { + settings.numSubtexturesX = std::max(1, numX); + changed = true; + } + if (ImGui::InputInt("Num Subtextures Y", &numY)) { + settings.numSubtexturesY = std::max(1, numY); + changed = true; + } + + ImGui::SeparatorText("Texture Path"); + char textureBuffer[256]; + strncpy_s(textureBuffer, settings.particleTexture.c_str(), sizeof(textureBuffer) - 1); + if (ImGui::InputText("Particle Texture", textureBuffer, sizeof(textureBuffer))) { + settings.particleTexture = textureBuffer; + changed = true; + } + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } + ImGui::End(); +} + +void PrecipitationWidget::LoadSettings() +{ + if (!precipitation) + return; + + if (!js.empty()) { + try { + if (js.contains("gravityVelocity")) settings.gravityVelocity = js["gravityVelocity"]; + if (js.contains("rotationVelocity")) settings.rotationVelocity = js["rotationVelocity"]; + if (js.contains("particleSizeX")) settings.particleSizeX = js["particleSizeX"]; + if (js.contains("particleSizeY")) settings.particleSizeY = js["particleSizeY"]; + if (js.contains("centerOffsetMin")) settings.centerOffsetMin = js["centerOffsetMin"]; + if (js.contains("centerOffsetMax")) settings.centerOffsetMax = js["centerOffsetMax"]; + if (js.contains("startRotationRange")) settings.startRotationRange = js["startRotationRange"]; + if (js.contains("numSubtexturesX")) settings.numSubtexturesX = js["numSubtexturesX"]; + if (js.contains("numSubtexturesY")) settings.numSubtexturesY = js["numSubtexturesY"]; + if (js.contains("particleType")) settings.particleType = js["particleType"]; + if (js.contains("boxSize")) settings.boxSize = js["boxSize"]; + if (js.contains("particleDensity")) settings.particleDensity = js["particleDensity"]; + if (js.contains("particleTexture")) settings.particleTexture = js["particleTexture"].get(); + } catch (const std::exception& e) { + logger::error("Precipitation {}: Failed to load from JSON: {}", GetEditorID(), e.what()); + } + } else { + auto& runtime = precipitation->GetRuntimeData(); + + settings.gravityVelocity = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kGravityVelocity).f; + settings.rotationVelocity = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kRotationVelocity).f; + settings.particleSizeX = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleSizeX).f; + settings.particleSizeY = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleSizeY).f; + settings.centerOffsetMin = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kCenterOffsetMin).f; + settings.centerOffsetMax = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kCenterOffsetMax).f; + settings.startRotationRange = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kStartRotationRange).f; + settings.numSubtexturesX = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kNumSubtexturesX).i; + settings.numSubtexturesY = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kNumSubtexturesY).i; + settings.particleType = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleType).i; + settings.boxSize = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kBoxSize).f; + settings.particleDensity = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleDensity).f; + settings.particleTexture = runtime.particleTexture.textureName.c_str(); + } + + originalSettings = settings; +} + +void PrecipitationWidget::SaveSettings() +{ + js["gravityVelocity"] = settings.gravityVelocity; + js["rotationVelocity"] = settings.rotationVelocity; + js["particleSizeX"] = settings.particleSizeX; + js["particleSizeY"] = settings.particleSizeY; + js["centerOffsetMin"] = settings.centerOffsetMin; + js["centerOffsetMax"] = settings.centerOffsetMax; + js["startRotationRange"] = settings.startRotationRange; + js["numSubtexturesX"] = settings.numSubtexturesX; + js["numSubtexturesY"] = settings.numSubtexturesY; + js["particleType"] = settings.particleType; + js["boxSize"] = settings.boxSize; + js["particleDensity"] = settings.particleDensity; + js["particleTexture"] = settings.particleTexture; +} + +void PrecipitationWidget::ApplyChanges() +{ + if (!precipitation) + return; + + auto& runtime = precipitation->GetRuntimeData(); + + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kGravityVelocity].f = settings.gravityVelocity; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kRotationVelocity].f = settings.rotationVelocity; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kParticleSizeX].f = settings.particleSizeX; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kParticleSizeY].f = settings.particleSizeY; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kCenterOffsetMin].f = settings.centerOffsetMin; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kCenterOffsetMax].f = settings.centerOffsetMax; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kStartRotationRange].f = settings.startRotationRange; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kNumSubtexturesX].i = settings.numSubtexturesX; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kNumSubtexturesY].i = settings.numSubtexturesY; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kParticleType].i = settings.particleType; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kBoxSize].f = settings.boxSize; + runtime.data[(uint32_t)RE::BGSShaderParticleGeometryData::DataID::kParticleDensity].f = settings.particleDensity; + runtime.particleTexture.textureName = settings.particleTexture.c_str(); + + originalSettings = settings; +} + +void PrecipitationWidget::RevertChanges() +{ + settings = originalSettings; +} + +bool PrecipitationWidget::HasUnsavedChanges() const +{ + return settings.gravityVelocity != originalSettings.gravityVelocity || + settings.rotationVelocity != originalSettings.rotationVelocity || + settings.particleSizeX != originalSettings.particleSizeX || + settings.particleSizeY != originalSettings.particleSizeY || + settings.centerOffsetMin != originalSettings.centerOffsetMin || + settings.centerOffsetMax != originalSettings.centerOffsetMax || + settings.startRotationRange != originalSettings.startRotationRange || + settings.numSubtexturesX != originalSettings.numSubtexturesX || + settings.numSubtexturesY != originalSettings.numSubtexturesY || + settings.particleType != originalSettings.particleType || + settings.boxSize != originalSettings.boxSize || + settings.particleDensity != originalSettings.particleDensity || + settings.particleTexture != originalSettings.particleTexture; +} diff --git a/src/WeatherEditor/Weather/PrecipitationWidget.h b/src/WeatherEditor/Weather/PrecipitationWidget.h new file mode 100644 index 0000000000..664fbd3063 --- /dev/null +++ b/src/WeatherEditor/Weather/PrecipitationWidget.h @@ -0,0 +1,45 @@ +#pragma once + +#include "../Widget.h" + +class PrecipitationWidget : public Widget +{ +public: + PrecipitationWidget(RE::BGSShaderParticleGeometryData* a_precipitation) : + precipitation(a_precipitation) + { + form = a_precipitation; + } + + ~PrecipitationWidget() override = default; + + void DrawWidget() override; + void LoadSettings() override; + void SaveSettings() override; + void ApplyChanges() override; + void RevertChanges() override; + bool HasUnsavedChanges() const override; + + RE::BGSShaderParticleGeometryData* precipitation = nullptr; + +private: + struct Settings + { + float gravityVelocity = 0.0f; + float rotationVelocity = 0.0f; + float particleSizeX = 1.0f; + float particleSizeY = 1.0f; + float centerOffsetMin = 0.0f; + float centerOffsetMax = 0.0f; + float startRotationRange = 0.0f; + uint32_t numSubtexturesX = 1; + uint32_t numSubtexturesY = 1; + uint32_t particleType = 0; // 0 = Rain, 1 = Snow + float boxSize = 1.0f; + float particleDensity = 1.0f; + std::string particleTexture = ""; + }; + + Settings settings; + Settings originalSettings; +}; diff --git a/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp b/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp new file mode 100644 index 0000000000..d7c8ce90f8 --- /dev/null +++ b/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp @@ -0,0 +1,123 @@ +#include "ReferenceEffectWidget.h" +#include "../EditorWindow.h" +#include "../WeatherUtils.h" + +void ReferenceEffectWidget::DrawWidget() +{ + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { + DrawWidgetHeader("##ReferenceEffectSearch", false, true); + + bool changed = false; + + auto editorWindow = EditorWindow::GetSingleton(); + + ImGui::SeparatorText("Art Object"); + if (editorWindow->artObjectWidgets.empty()) { + ImGui::TextDisabled("No Art Objects available"); + } else { + if (WeatherUtils::DrawFormPickerCached("Art Object", settings.artObject, editorWindow->artObjectWidgets, false, true)) changed = true; + } + + ImGui::SeparatorText("Effect Shader"); + if (editorWindow->effectShaderWidgets.empty()) { + ImGui::TextDisabled("No Effect Shaders available"); + } else { + if (WeatherUtils::DrawFormPickerCached("Effect Shader", settings.effectShader, editorWindow->effectShaderWidgets, false, true)) changed = true; + } + + ImGui::SeparatorText("Flags"); + if (ImGui::Checkbox("Face Target", &settings.faceTarget)) changed = true; + if (ImGui::Checkbox("Attach To Camera", &settings.attachToCamera)) changed = true; + if (ImGui::Checkbox("Inherit Rotation", &settings.inheritRotation)) changed = true; + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } + ImGui::End(); +} + +void ReferenceEffectWidget::LoadSettings() +{ + if (!referenceEffect) + return; + + if (!js.empty()) { + try { + if (js.contains("artObject")) { + std::string formIDStr = js["artObject"].get(); + if (formIDStr != "00000000") { + uint32_t formID = std::stoul(formIDStr, nullptr, 16); + settings.artObject = RE::TESForm::LookupByID(formID); + } else { + settings.artObject = nullptr; + } + } + if (js.contains("effectShader")) { + std::string formIDStr = js["effectShader"].get(); + if (formIDStr != "00000000") { + uint32_t formID = std::stoul(formIDStr, nullptr, 16); + settings.effectShader = RE::TESForm::LookupByID(formID); + } else { + settings.effectShader = nullptr; + } + } + if (js.contains("faceTarget")) settings.faceTarget = js["faceTarget"]; + if (js.contains("attachToCamera")) settings.attachToCamera = js["attachToCamera"]; + if (js.contains("inheritRotation")) settings.inheritRotation = js["inheritRotation"]; + } catch (const std::exception& e) { + logger::error("ReferenceEffect {}: Failed to load from JSON: {}", GetEditorID(), e.what()); + } + } else { + settings.artObject = referenceEffect->data.artObject; + settings.effectShader = referenceEffect->data.effectShader; + settings.faceTarget = referenceEffect->data.flags.any(RE::BGSReferenceEffect::Flag::kFaceTarget); + settings.attachToCamera = referenceEffect->data.flags.any(RE::BGSReferenceEffect::Flag::kAttachToCamera); + settings.inheritRotation = referenceEffect->data.flags.any(RE::BGSReferenceEffect::Flag::kInheritRotation); + } + + originalSettings = settings; +} + +void ReferenceEffectWidget::SaveSettings() +{ + js["artObject"] = settings.artObject ? std::format("{:08X}", settings.artObject->GetFormID()) : "00000000"; + js["effectShader"] = settings.effectShader ? std::format("{:08X}", settings.effectShader->GetFormID()) : "00000000"; + js["faceTarget"] = settings.faceTarget; + js["attachToCamera"] = settings.attachToCamera; + js["inheritRotation"] = settings.inheritRotation; +} + +void ReferenceEffectWidget::ApplyChanges() +{ + if (!referenceEffect) + return; + + referenceEffect->data.artObject = settings.artObject; + referenceEffect->data.effectShader = settings.effectShader; + + referenceEffect->data.flags.reset(); + if (settings.faceTarget) + referenceEffect->data.flags.set(RE::BGSReferenceEffect::Flag::kFaceTarget); + if (settings.attachToCamera) + referenceEffect->data.flags.set(RE::BGSReferenceEffect::Flag::kAttachToCamera); + if (settings.inheritRotation) + referenceEffect->data.flags.set(RE::BGSReferenceEffect::Flag::kInheritRotation); + + originalSettings = settings; +} + +void ReferenceEffectWidget::RevertChanges() +{ + settings = originalSettings; +} + +bool ReferenceEffectWidget::HasUnsavedChanges() const +{ + return settings.artObject != originalSettings.artObject || + settings.effectShader != originalSettings.effectShader || + settings.faceTarget != originalSettings.faceTarget || + settings.attachToCamera != originalSettings.attachToCamera || + settings.inheritRotation != originalSettings.inheritRotation; +} diff --git a/src/WeatherEditor/Weather/ReferenceEffectWidget.h b/src/WeatherEditor/Weather/ReferenceEffectWidget.h new file mode 100644 index 0000000000..cb7f5c2fd0 --- /dev/null +++ b/src/WeatherEditor/Weather/ReferenceEffectWidget.h @@ -0,0 +1,40 @@ +#pragma once + +#include "../Widget.h" + +class ReferenceEffectWidget : public Widget +{ +public: + ReferenceEffectWidget(RE::BGSReferenceEffect* a_referenceEffect) : + referenceEffect(a_referenceEffect) + { + form = a_referenceEffect; + } + + ~ReferenceEffectWidget() override = default; + + void DrawWidget() override; + void LoadSettings() override; + void SaveSettings() override; + void ApplyChanges() override; + void RevertChanges() override; + bool HasUnsavedChanges() const override; + + RE::BGSReferenceEffect* referenceEffect = nullptr; + +private: + struct Settings + { + RE::BGSArtObject* artObject = nullptr; + RE::TESEffectShader* effectShader = nullptr; + bool faceTarget = false; + bool attachToCamera = false; + bool inheritRotation = false; + }; + + Settings settings; + Settings originalSettings; + + std::vector artObjectArray; + std::vector effectShaderArray; +}; diff --git a/src/WeatherEditor/Weather/SimpleFormWidget.h b/src/WeatherEditor/Weather/SimpleFormWidget.h new file mode 100644 index 0000000000..6bad8154a8 --- /dev/null +++ b/src/WeatherEditor/Weather/SimpleFormWidget.h @@ -0,0 +1,40 @@ +#pragma once + +#include "../Widget.h" + +// Simple widget for displaying form information without editing +class SimpleFormWidget : public Widget +{ +public: + SimpleFormWidget() = default; + ~SimpleFormWidget() override = default; + + void SetFormData(const char* editorID, uint32_t formID, const char* filename) + { + this->editorID = editorID ? editorID : ""; + this->formID = formID; + this->filename = filename ? filename : "Generated"; + } + + std::string GetEditorID() const override { return editorID; } + std::string GetFormID() const override { return std::format("{:08X}", formID); } + std::string GetFilename() const override { return filename; } + + void DrawWidget() override + { + ImGui::Text("EditorID: %s", editorID.c_str()); + ImGui::Text("FormID: %08X", formID); + ImGui::Text("File: %s", filename.c_str()); + ImGui::Separator(); + ImGui::TextWrapped("This form is referenced by weather records. To change which form is used, edit the Records tab in the Weather widget."); + } + + void LoadSettings() override {} + void SaveSettings() override {} + void ApplyChanges() override {} + +private: + std::string editorID; + uint32_t formID = 0; + std::string filename; +}; diff --git a/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp b/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp new file mode 100644 index 0000000000..09a37765b7 --- /dev/null +++ b/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp @@ -0,0 +1,156 @@ +#include "VolumetricLightingWidget.h" +#include "../EditorWindow.h" +#include "../WeatherUtils.h" + +void VolumetricLightingWidget::DrawWidget() +{ + ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { + DrawWidgetHeader("##VolumetricLightingSearch", false, true); + + bool changed = false; + + if (ImGui::BeginTabBar("VolumetricLightingTabs")) { + if (ImGui::BeginTabItem("Basic")) { + ImGui::SeparatorText("Intensity"); + if (WeatherUtils::DrawSliderFloat("Intensity", settings.intensity, 0.0f, 10.0f)) changed = true; + + ImGui::SeparatorText("Custom Color"); + if (WeatherUtils::DrawSliderFloat("Contribution", settings.customColorContribution, 0.0f, 1.0f)) changed = true; + + ImGui::SeparatorText("RGB Color"); + if (WeatherUtils::DrawSliderFloat("Red", settings.red, 0.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Green", settings.green, 0.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Blue", settings.blue, 0.0f, 1.0f)) changed = true; + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Density")) { + ImGui::SeparatorText("Density Settings"); + if (WeatherUtils::DrawSliderFloat("Contribution", settings.densityContribution, 0.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Size", settings.densitySize, 0.0f, 10.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Wind Speed", settings.densityWindSpeed, -100.0f, 100.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Falling Speed", settings.densityFallingSpeed, -100.0f, 100.0f)) changed = true; + + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Advanced")) { + ImGui::SeparatorText("Phase Function"); + if (WeatherUtils::DrawSliderFloat("Contribution", settings.phaseFunctionContribution, 0.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Scattering", settings.phaseFunctionScattering, -1.0f, 1.0f)) changed = true; + + ImGui::SeparatorText("Sampling"); + if (WeatherUtils::DrawSliderFloat("Range Factor", settings.samplingRangeFactor, 0.0f, 10.0f)) changed = true; + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + } + ImGui::End(); +} + +void VolumetricLightingWidget::LoadSettings() +{ + if (!volumetricLighting) + return; + + if (!js.empty()) { + try { + if (js.contains("intensity")) settings.intensity = js["intensity"]; + if (js.contains("customColorContribution")) settings.customColorContribution = js["customColorContribution"]; + if (js.contains("red")) settings.red = js["red"]; + if (js.contains("green")) settings.green = js["green"]; + if (js.contains("blue")) settings.blue = js["blue"]; + if (js.contains("densityContribution")) settings.densityContribution = js["densityContribution"]; + if (js.contains("densitySize")) settings.densitySize = js["densitySize"]; + if (js.contains("densityWindSpeed")) settings.densityWindSpeed = js["densityWindSpeed"]; + if (js.contains("densityFallingSpeed")) settings.densityFallingSpeed = js["densityFallingSpeed"]; + if (js.contains("phaseFunctionContribution")) settings.phaseFunctionContribution = js["phaseFunctionContribution"]; + if (js.contains("phaseFunctionScattering")) settings.phaseFunctionScattering = js["phaseFunctionScattering"]; + if (js.contains("samplingRangeFactor")) settings.samplingRangeFactor = js["samplingRangeFactor"]; + } catch (const std::exception& e) { + logger::error("VolumetricLighting {}: Failed to load from JSON: {}", GetEditorID(), e.what()); + } + } else { + settings.intensity = volumetricLighting->intensity; + settings.customColorContribution = volumetricLighting->customColor.contribution; + settings.red = volumetricLighting->red; + settings.green = volumetricLighting->green; + settings.blue = volumetricLighting->blue; + settings.densityContribution = volumetricLighting->density.contribution; + settings.densitySize = volumetricLighting->density.size; + settings.densityWindSpeed = volumetricLighting->density.windSpeed; + settings.densityFallingSpeed = volumetricLighting->density.fallingSpeed; + settings.phaseFunctionContribution = volumetricLighting->phaseFunction.contribution; + settings.phaseFunctionScattering = volumetricLighting->phaseFunction.scattering; + settings.samplingRangeFactor = volumetricLighting->samplingRepartition.rangeFactor; + } + + originalSettings = settings; +} + +void VolumetricLightingWidget::SaveSettings() +{ + js["intensity"] = settings.intensity; + js["customColorContribution"] = settings.customColorContribution; + js["red"] = settings.red; + js["green"] = settings.green; + js["blue"] = settings.blue; + js["densityContribution"] = settings.densityContribution; + js["densitySize"] = settings.densitySize; + js["densityWindSpeed"] = settings.densityWindSpeed; + js["densityFallingSpeed"] = settings.densityFallingSpeed; + js["phaseFunctionContribution"] = settings.phaseFunctionContribution; + js["phaseFunctionScattering"] = settings.phaseFunctionScattering; + js["samplingRangeFactor"] = settings.samplingRangeFactor; +} + +void VolumetricLightingWidget::ApplyChanges() +{ + if (!volumetricLighting) + return; + + volumetricLighting->intensity = settings.intensity; + volumetricLighting->customColor.contribution = settings.customColorContribution; + volumetricLighting->red = settings.red; + volumetricLighting->green = settings.green; + volumetricLighting->blue = settings.blue; + volumetricLighting->density.contribution = settings.densityContribution; + volumetricLighting->density.size = settings.densitySize; + volumetricLighting->density.windSpeed = settings.densityWindSpeed; + volumetricLighting->density.fallingSpeed = settings.densityFallingSpeed; + volumetricLighting->phaseFunction.contribution = settings.phaseFunctionContribution; + volumetricLighting->phaseFunction.scattering = settings.phaseFunctionScattering; + volumetricLighting->samplingRepartition.rangeFactor = settings.samplingRangeFactor; + + originalSettings = settings; +} + +void VolumetricLightingWidget::RevertChanges() +{ + settings = originalSettings; +} + +bool VolumetricLightingWidget::HasUnsavedChanges() const +{ + return settings.intensity != originalSettings.intensity || + settings.customColorContribution != originalSettings.customColorContribution || + settings.red != originalSettings.red || + settings.green != originalSettings.green || + settings.blue != originalSettings.blue || + settings.densityContribution != originalSettings.densityContribution || + settings.densitySize != originalSettings.densitySize || + settings.densityWindSpeed != originalSettings.densityWindSpeed || + settings.densityFallingSpeed != originalSettings.densityFallingSpeed || + settings.phaseFunctionContribution != originalSettings.phaseFunctionContribution || + settings.phaseFunctionScattering != originalSettings.phaseFunctionScattering || + settings.samplingRangeFactor != originalSettings.samplingRangeFactor; +} diff --git a/src/WeatherEditor/Weather/VolumetricLightingWidget.h b/src/WeatherEditor/Weather/VolumetricLightingWidget.h new file mode 100644 index 0000000000..cc1b158e73 --- /dev/null +++ b/src/WeatherEditor/Weather/VolumetricLightingWidget.h @@ -0,0 +1,44 @@ +#pragma once + +#include "../Widget.h" + +class VolumetricLightingWidget : public Widget +{ +public: + VolumetricLightingWidget(RE::BGSVolumetricLighting* a_volumetricLighting) : + volumetricLighting(a_volumetricLighting) + { + form = a_volumetricLighting; + } + + ~VolumetricLightingWidget() override = default; + + void DrawWidget() override; + void LoadSettings() override; + void SaveSettings() override; + void ApplyChanges() override; + void RevertChanges() override; + bool HasUnsavedChanges() const override; + + RE::BGSVolumetricLighting* volumetricLighting = nullptr; + +private: + struct Settings + { + float intensity = 1.0f; + float customColorContribution = 0.0f; + float red = 1.0f; + float green = 1.0f; + float blue = 1.0f; + float densityContribution = 0.5f; + float densitySize = 1.0f; + float densityWindSpeed = 0.0f; + float densityFallingSpeed = 0.0f; + float phaseFunctionContribution = 0.0f; + float phaseFunctionScattering = 0.0f; + float samplingRangeFactor = 1.0f; + }; + + Settings settings; + Settings originalSettings; +}; diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index c9d1e230b4..bd6e8549e1 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -1,14 +1,17 @@ #include "WeatherWidget.h" +#include + #include "../EditorWindow.h" #include "State.h" +#include "Utils/UI.h" #include "WeatherManager.h" #include "WeatherVariableRegistry.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Atmosphere, colorTimes) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DirectionalColor, max, min) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::DALC, specular, fresnelPower, directional) -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Cloud, cloudLayerSpeedY, cloudLayerSpeedX, color, cloudAlpha) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Cloud, cloudLayerSpeedY, cloudLayerSpeedX, color, cloudAlpha, enabled, texturePath) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::ImageSpaceSettings, hdrEyeAdaptSpeed, @@ -28,7 +31,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::ImageSpaceSettings, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Settings, parent, - inheritance, + inheritFlags, weatherProperties, weatherColors, fogProperties, @@ -40,6 +43,12 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WeatherWidget::Settings, WeatherWidget::~WeatherWidget() { + for (auto& [layerIndex, srv] : cloudTextureCache) { + if (srv) { + srv->Release(); + } + } + cloudTextureCache.clear(); } void WeatherWidget::DrawWidget() @@ -61,6 +70,8 @@ void WeatherWidget::DrawWidget() ImGui::SetNextWindowPos(ImVec2(ImGui::GetCursorScreenPos().x, ImGui::GetCursorScreenPos().y)); ImGui::SetNextWindowSize(ImVec2(200.0f * 1.5f, 0)); ImGui::SetNextWindowFocus(); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.16f, 0.16f, 0.16f, 1.0f)); if (ImGui::Begin("##SearchDropdown", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { @@ -88,18 +99,21 @@ void WeatherWidget::DrawWidget() } } ImGui::End(); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); } auto editorWindow = EditorWindow::GetSingleton(); auto& widgets = editorWindow->weatherWidgets; // Sets the parent widget if settings have been loaded. - if (settings.parent != "None") { - parent = GetParent(); - if (parent == nullptr) - settings.parent = "None"; - } + if (settings.parent != "None") { + parent = GetParent(); + if (parent == nullptr) + settings.parent = "None"; + } + if (editorWindow->settings.enableInheritFromParent) { if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { // Option for "None" if (ImGui::Selectable("None", parent == nullptr)) { @@ -111,30 +125,49 @@ void WeatherWidget::DrawWidget() auto& widget = widgets[i]; // Skip self-selection - if (widget == this) + if (widget.get() == this) continue; // Option for each widget - if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget)) { - parent = (WeatherWidget*)widget; + if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget.get())) { + parent = (WeatherWidget*)widget.get(); settings.parent = widget->GetEditorID(); } // Set default focus to the current parent - if (parent == widget) { + if (parent == widget.get()) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); + ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "Note: This is NOT the same as cell lighting template inheritance."); + ImGui::EndTooltip(); + } - if (parent && !parent->IsOpen()) { + if (parent) { ImGui::SameLine(); - if (ImGui::Button("Open")) - parent->SetOpen(true); + if (ImGui::Button("Inherit All")) { + InheritAllFromParent(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Copy all parameter values from parent weather"); + } + + if (!parent->IsOpen()) { + ImGui::SameLine(); + if (ImGui::Button("Open")) + parent->SetOpen(true); + } } - - // Tab bar for organizing settings + } +} // Tab bar for organizing settings if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { // Use activeTabOverride to auto-navigate to specific tab ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; @@ -143,7 +176,7 @@ void WeatherWidget::DrawWidget() ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags imageSpaceFlags = (activeTabOverride == "ImageSpace") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags recordsFlags = (activeTabOverride == "Records") ? ImGuiTabItemFlags_SetSelected : 0; if (!activeTabOverride.empty()) { activeTabOverride = ""; // Clear after use } @@ -153,13 +186,11 @@ void WeatherWidget::DrawWidget() DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, - { "Thunder Lightning Frequency", INT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); + { "Thunder Lightning Frequency", INT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { + } if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { DrawDALCSettings(); ImGui::EndTabItem(); } @@ -184,22 +215,202 @@ void WeatherWidget::DrawWidget() ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("ImageSpace", nullptr, imageSpaceFlags)) { - DrawImageSpaceSettings(); - ImGui::EndTabItem(); + if (ImGui::BeginTabItem("Records", nullptr, recordsFlags)) { + ImGui::Spacing(); + ImGui::TextWrapped("Form record references used by this weather."); + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + auto* editorWindow = EditorWindow::GetSingleton(); + + bool recordChanged = false; + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; + + // ImageSpace Records (per time of day) + if (ImGui::CollapsingHeader("ImageSpace", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "ImageSpace_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i]; + recordChanged = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); + } + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button + if (weather->imageSpaces[i]) { + ImGui::SameLine(); + if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { + for (auto& widget : editorWindow->imageSpaceWidgets) { + if (widget->form == weather->imageSpaces[i]) { + widget->SetOpen(true); + break; + } + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Open this ImageSpace for editing"); + } + } + + ImGui::PopID(); + } + ImGui::Spacing(); + } + + // Volumetric Lighting Records (per time of day) + if (ImGui::CollapsingHeader("Volumetric Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(100 + i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "VolumetricLighting_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i]; + recordChanged = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); + } + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button + if (weather->volumetricLighting[i]) { + ImGui::SameLine(); + if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { + for (auto& widget : editorWindow->volumetricLightingWidgets) { + if (widget->form == weather->volumetricLighting[i]) { + widget->SetOpen(true); + break; + } + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Open this Volumetric Lighting for editing"); + } + } + + ImGui::PopID(); + } + ImGui::Spacing(); + } + + // Precipitation Data + if (ImGui::CollapsingHeader("Precipitation", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["Precipitation"]; + if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->precipitationData = parentWidget->weather->precipitationData; + recordChanged = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); + } + + ImGui::Text("Particle Shader:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button + if (weather->precipitationData) { + ImGui::SameLine(); + if (ImGui::SmallButton("Open##Precip")) { + for (auto& widget : editorWindow->precipitationWidgets) { + if (widget->form == weather->precipitationData) { + widget->SetOpen(true); + break; + } + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Open this Precipitation for editing"); + } + } + + ImGui::Spacing(); + } + + // Visual Effect (Reference Effect) + if (ImGui::CollapsingHeader("Visual Effect", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["ReferenceEffect"]; + if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->referenceEffect = parentWidget->weather->referenceEffect; + recordChanged = true; + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } + + ImGui::Text("Reference Effect:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button + if (weather->referenceEffect) { + ImGui::SameLine(); + if (ImGui::SmallButton("Open##RefEffect")) { + for (auto& widget : editorWindow->referenceEffectWidgets) { + if (widget->form == weather->referenceEffect) { + widget->SetOpen(true); + break; + } + } + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Open this Visual Effect for editing"); + } + } + + + ImGui::Spacing(); + } - ImGui::EndTabBar(); + if (recordChanged) { } + + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } ImGui::End(); } void WeatherWidget::LoadSettings() { - bool hadErrors = false; - - if (!js.empty()) { + bool hadErrors = false; if (!js.empty()) { try { // Attempt to load settings from JSON settings = js; @@ -235,7 +446,6 @@ void WeatherWidget::LoadSettings() } catch (const nlohmann::json::exception& e) { logger::error("Weather {}: Failed to deserialize settings from JSON: {}", GetEditorID(), e.what()); - logger::error("JSON content: {}", js.dump(2)); // Fallback to vanilla/game values on exception LoadWeatherValues(); EditorWindow::GetSingleton()->ShowNotification( @@ -245,8 +455,6 @@ void WeatherWidget::LoadSettings() return; } } else { - // No JSON data, restore cached vanilla values - logger::info("Weather {}: No JSON data, restoring cached vanilla values", GetEditorID()); settings = vanillaSettings; } LoadFeatureSettings(); @@ -259,14 +467,6 @@ void WeatherWidget::SaveSettings() try { js = settings; - // Log what we're saving for debugging - logger::info("Weather {}: Saving settings - {} weather properties, {} colors, {} fog properties", - GetEditorID(), - settings.weatherProperties.size(), - settings.weatherColors.size(), - settings.fogProperties.size()); - - // Validate serialization worked if (js.is_null()) { logger::error("Weather {}: Serialization produced null JSON!", GetEditorID()); } else if (!js.contains("weatherProperties")) { @@ -287,9 +487,9 @@ WeatherWidget* WeatherWidget::GetParent() auto editorWindow = EditorWindow::GetSingleton(); auto& widgets = editorWindow->weatherWidgets; - auto temp = std::find_if(widgets.begin(), widgets.end(), [&](Widget* w) { return w->GetEditorID() == settings.parent; }); + auto temp = std::find_if(widgets.begin(), widgets.end(), [&](const auto& w) { return w->GetEditorID() == settings.parent; }); if (temp != widgets.end()) - return (WeatherWidget*)*temp; + return (WeatherWidget*)temp->get(); return nullptr; } @@ -371,11 +571,16 @@ void WeatherWidget::SetWeatherValues() } // Clouds + uint32_t disabledBits = 0; for (size_t i = 0; i < TESWeather::kTotalLayers; i++) { auto& settingsCloud = settings.clouds[i]; weather->cloudLayerSpeedX[i] = (int8_t)settingsCloud.cloudLayerSpeedX; weather->cloudLayerSpeedY[i] = (int8_t)settingsCloud.cloudLayerSpeedY; + + if (!settingsCloud.enabled) { + disabledBits |= (1 << i); + } auto& cloudColors = weather->cloudColorData[i]; auto& cloudAlphas = weather->cloudAlpha[i]; @@ -385,9 +590,7 @@ void WeatherWidget::SetWeatherValues() Float3ToColor(settingsCloud.color[j], cloudColors[j]); } } - - // ImageSpace - SetImageSpaceValues(); + weather->cloudLayerDisabledBits = disabledBits; } void WeatherWidget::LoadWeatherValues() @@ -467,6 +670,8 @@ void WeatherWidget::LoadWeatherValues() settingsCloud.cloudLayerSpeedX = weather->cloudLayerSpeedX[i]; settingsCloud.cloudLayerSpeedY = weather->cloudLayerSpeedY[i]; + settingsCloud.enabled = !(weather->cloudLayerDisabledBits & (1 << i)); + settingsCloud.texturePath = weather->cloudTextures[i].textureName.c_str(); auto& cloudColors = weather->cloudColorData[i]; auto& cloudAlphas = weather->cloudAlpha[i]; @@ -476,294 +681,504 @@ void WeatherWidget::LoadWeatherValues() ColorToFloat3(cloudColors[j], settingsCloud.color[j]); } } - - // ImageSpace - LoadImageSpaceValues(); } void WeatherWidget::DrawDALCSettings() { - bool& doesInherit = settings.inheritance["DALC"]; - ImGui::Checkbox("Inherit From Parent##dalc", &doesInherit); + auto editorWindow = EditorWindow::GetSingleton(); + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; - if (doesInherit && HasParent()) { - for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { - settings.dalc[i] = GetParent()->settings.dalc[i]; - } - } else { - doesInherit = false; - bool changed = false; + bool changed = false; - if (TOD::BeginTODTable("DALC_TOD_Table")) { - TOD::RenderTODHeader(); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + if (TOD::BeginTODTable("DALC_TOD_Table")) { + TOD::RenderTODHeader(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); - // Prepare arrays for TOD rendering - float3 specularColors[4]; - float fresnelPowers[4]; - float3 directionalXMax[4], directionalXMin[4]; - float3 directionalYMax[4], directionalYMin[4]; - float3 directionalZMax[4], directionalZMin[4]; + // Prepare arrays for TOD rendering + float3 specularColors[4]; + float fresnelPowers[4]; + float3 directionalXMax[4], directionalXMin[4]; + float3 directionalYMax[4], directionalYMin[4]; + float3 directionalZMax[4], directionalZMin[4]; + + // Parent values + float3 parentSpecular[4] = {}; + float parentFresnel[4] = {}; + float3 parentDirXMax[4] = {}, parentDirXMin[4] = {}; + float3 parentDirYMax[4] = {}, parentDirYMin[4] = {}; + float3 parentDirZMax[4] = {}, parentDirZMin[4] = {}; - for (int i = 0; i < ColorTimes::kTotal; i++) { - specularColors[i] = settings.dalc[i].specular; - fresnelPowers[i] = settings.dalc[i].fresnelPower; - directionalXMax[i] = settings.dalc[i].directional[0].max; - directionalXMin[i] = settings.dalc[i].directional[0].min; - directionalYMax[i] = settings.dalc[i].directional[1].max; - directionalYMin[i] = settings.dalc[i].directional[1].min; - directionalZMax[i] = settings.dalc[i].directional[2].max; - directionalZMin[i] = settings.dalc[i].directional[2].min; + for (int i = 0; i < ColorTimes::kTotal; i++) { + specularColors[i] = settings.dalc[i].specular; + fresnelPowers[i] = settings.dalc[i].fresnelPower; + directionalXMax[i] = settings.dalc[i].directional[0].max; + directionalXMin[i] = settings.dalc[i].directional[0].min; + directionalYMax[i] = settings.dalc[i].directional[1].max; + directionalYMin[i] = settings.dalc[i].directional[1].min; + directionalZMax[i] = settings.dalc[i].directional[2].max; + directionalZMin[i] = settings.dalc[i].directional[2].min; + + if (parentWidget) { + parentSpecular[i] = parentWidget->settings.dalc[i].specular; + parentFresnel[i] = parentWidget->settings.dalc[i].fresnelPower; + parentDirXMax[i] = parentWidget->settings.dalc[i].directional[0].max; + parentDirXMin[i] = parentWidget->settings.dalc[i].directional[0].min; + parentDirYMax[i] = parentWidget->settings.dalc[i].directional[1].max; + parentDirYMin[i] = parentWidget->settings.dalc[i].directional[1].min; + parentDirZMax[i] = parentWidget->settings.dalc[i].directional[2].max; + parentDirZMin[i] = parentWidget->settings.dalc[i].directional[2].min; + } + } + + // Draw with per-parameter inheritance + if (hasParent) { + if (TOD::DrawTODColorRow("Specular", specularColors, settings.inheritFlags["DALC_Specular"], parentSpecular)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].specular = specularColors[i]; + changed = true; } + if (TOD::DrawTODFloatRow("Fresnel Power", fresnelPowers, settings.inheritFlags["DALC_Fresnel"], parentFresnel, 0.0f, 10.0f)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].fresnelPower = fresnelPowers[i]; + changed = true; + } + } else { if (TOD::DrawTODColorRow("Specular", specularColors)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].specular = specularColors[i]; changed = true; } - if (TOD::DrawTODSliderRow("Fresnel Power", fresnelPowers, 0.0f, 10.0f)) { + if (TOD::DrawTODFloatRow("Fresnel Power", fresnelPowers, 0.0f, 10.0f)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].fresnelPower = fresnelPowers[i]; changed = true; } + } - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); - if (TOD::DrawTODColorRow("Directional X Max", directionalXMax)) { + // Directional colors with per-parameter inheritance + if (hasParent) { + if (TOD::DrawTODColorRow("Directional +X", directionalXMax, settings.inheritFlags["DALC_DirXMax"], parentDirXMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[0].max = directionalXMax[i]; changed = true; } - if (TOD::DrawTODColorRow("Directional X Min", directionalXMin)) { + if (TOD::DrawTODColorRow("Directional -X", directionalXMin, settings.inheritFlags["DALC_DirXMin"], parentDirXMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[0].min = directionalXMin[i]; changed = true; } - if (TOD::DrawTODColorRow("Directional Y Max", directionalYMax)) { + if (TOD::DrawTODColorRow("Directional +Y", directionalYMax, settings.inheritFlags["DALC_DirYMax"], parentDirYMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[1].max = directionalYMax[i]; changed = true; } - if (TOD::DrawTODColorRow("Directional Y Min", directionalYMin)) { + if (TOD::DrawTODColorRow("Directional -Y", directionalYMin, settings.inheritFlags["DALC_DirYMin"], parentDirYMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[1].min = directionalYMin[i]; changed = true; } - if (TOD::DrawTODColorRow("Directional Z Max", directionalZMax)) { + if (TOD::DrawTODColorRow("Directional +Z", directionalZMax, settings.inheritFlags["DALC_DirZMax"], parentDirZMax)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[2].max = directionalZMax[i]; changed = true; } - if (TOD::DrawTODColorRow("Directional Z Min", directionalZMin)) { + if (TOD::DrawTODColorRow("Directional -Z", directionalZMin, settings.inheritFlags["DALC_DirZMin"], parentDirZMin)) { for (int i = 0; i < ColorTimes::kTotal; i++) settings.dalc[i].directional[2].min = directionalZMin[i]; changed = true; } + } else { + if (TOD::DrawTODColorRow("Directional +X", directionalXMax)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[0].max = directionalXMax[i]; + changed = true; + } - TOD::EndTODTable(); - } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); + if (TOD::DrawTODColorRow("Directional -X", directionalXMin)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[0].min = directionalXMin[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional +Y", directionalYMax)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[1].max = directionalYMax[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional -Y", directionalYMin)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[1].min = directionalYMin[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional +Z", directionalZMax)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[2].max = directionalZMax[i]; + changed = true; + } + + if (TOD::DrawTODColorRow("Directional -Z", directionalZMin)) { + for (int i = 0; i < ColorTimes::kTotal; i++) + settings.dalc[i].directional[2].min = directionalZMin[i]; + changed = true; + } } + + TOD::EndTODTable(); + } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } void WeatherWidget::DrawWeatherColorSettings() { - bool& doesInherit = settings.inheritance["Atmosphere Colors"]; - ImGui::Checkbox("Inherit From Parent##atmosphereColors", &doesInherit); + auto editorWindow = EditorWindow::GetSingleton(); + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; - if (&doesInherit && HasParent()) { - for (size_t i = 0; i < ColorTypes::kTotal; i++) { - settings.atmosphereColors[i] = GetParent()->settings.atmosphereColors[i]; - } - } else { - doesInherit = false; - bool changed = false; + bool changed = false; - if (TOD::BeginTODTable("AtmosphereColors_Table")) { - TOD::RenderTODHeader(); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + if (TOD::BeginTODTable("AtmosphereColors_Table")) { + TOD::RenderTODHeader(); - for (int i = 0; i < ColorTypes::kTotal; i++) { - std::string colorTypeLabel = ColorTypeLabel(i); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + // Organized display order: group related sky/fog/lighting properties + static const int displayOrder[] = { + 0, // Sky Upper + 7, // Sky Lower + 8, // Horizon + 1, // Fog Near + 12, // Fog Far + 3, // Ambient + 4, // Sunlight + 5, // Sun + 6, // Stars + 9, // Effect Lighting + 10, // Cloud LOD Diffuse + 11, // Cloud LOD Ambient + 13, // Sky Statics + 14, // Water Multiplier + 15, // Sun Glare + 16, // Moon Glare + 2, // Unknown + }; + + for (int idx = 0; idx < ColorTypes::kTotal; idx++) { + int i = displayOrder[idx]; + std::string colorTypeLabel = ColorTypeLabel(i); + + if (hasParent) { + float3 parentColors[4]; + for (int j = 0; j < 4; j++) + parentColors[j] = parentWidget->settings.atmosphereColors[i].colorTimes[j]; + + std::string inheritKey = "Atmosphere_" + colorTypeLabel; + if (TOD::DrawTODColorRow(colorTypeLabel.c_str(), settings.atmosphereColors[i].colorTimes, settings.inheritFlags[inheritKey], parentColors)) { + changed = true; + } + } else { if (TOD::DrawTODColorRow(colorTypeLabel.c_str(), settings.atmosphereColors[i].colorTimes)) { changed = true; } } - - TOD::EndTODTable(); } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); - } + TOD::EndTODTable(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } void WeatherWidget::DrawCloudSettings() { - bool& doesInherit = settings.inheritance["Clouds"]; - ImGui::Checkbox("Inherit From Parent##cloud", &doesInherit); + auto editorWindow = EditorWindow::GetSingleton(); + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; - if (doesInherit && HasParent()) { - for (size_t i = 0; i < RE::TESWeather::ColorTimes::kTotal; i++) { - settings.dalc[i] = GetParent()->settings.dalc[i]; + bool changed = false; + for (int i = 0; i < TESWeather::kTotalLayers; i++) { + std::string layer = std::format("Layer {}", i); + + // Default to open if this layer has a texture (is being used) + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (!settings.clouds[i].texturePath.empty()) { + flags |= ImGuiTreeNodeFlags_DefaultOpen; } - } else { - doesInherit = false; - bool changed = false; - for (int i = 0; i < TESWeather::kTotalLayers; i++) { - std::string layer = std::format("Layer {}", i); - if (ImGui::CollapsingHeader(layer.c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - if (DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; - ImGui::Spacing(); - if (DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; - ImGui::Spacing(); + if (ImGui::CollapsingHeader(layer.c_str(), flags)) { + ImGui::Indent(10.0f); + ImGui::Spacing(); + + bool layerEnabled = settings.clouds[i].enabled; + + // Begin horizontal layout for enable checkbox and sliders on left, texture on right + ImGui::BeginGroup(); + + if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { + settings.clouds[i].enabled = layerEnabled; + changed = true; + } + + ImGui::Spacing(); + ImGui::Spacing(); + + // Make sliders 1/3 width + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.4f); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; + ImGui::Spacing(); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; + ImGui::PopItemWidth(); + + ImGui::EndGroup(); + + // Draw texture in upper right if available + if (!settings.clouds[i].texturePath.empty()) { + auto* texture = GetCloudTexture(i); + if (texture) { + ImGui::SameLine(0.0f, 20.0f); + ImGui::BeginGroup(); + float textureSize = 128.0f; + ImGui::Image((void*)texture, ImVec2(textureSize, textureSize)); + // Small grey subtext below image + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushFont(ImGui::GetFont()); + ImGui::SetWindowFontScale(0.8f); + float textWidth = ImGui::CalcTextSize(settings.clouds[i].texturePath.c_str()).x; + if (textWidth > textureSize) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + textureSize); + ImGui::TextWrapped("%s", settings.clouds[i].texturePath.c_str()); + ImGui::PopTextWrapPos(); + } else { + ImGui::Text("%s", settings.clouds[i].texturePath.c_str()); + } + ImGui::SetWindowFontScale(1.0f); + ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::EndGroup(); + } + } + + ImGui::Spacing(); + ImGui::Spacing(); if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { + TOD::RenderTODHeader(); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + + if (hasParent) { + float3 parentColors[4]; + float parentAlphas[4]; + for (int j = 0; j < 4; j++) { + parentColors[j] = parentWidget->settings.clouds[i].color[j]; + parentAlphas[j] = parentWidget->settings.clouds[i].cloudAlpha[j]; + } - if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { - TOD::RenderTODHeader(); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + std::string colorKey = std::format("Cloud{}_Color", i); + std::string alphaKey = std::format("Cloud{}_Alpha", i); - if (TOD::DrawTODColorRow("Cloud Color", settings.clouds[i].color)) { + if (TOD::DrawTODColorRow("Cloud Color", settings.clouds[i].color, settings.inheritFlags[colorKey], parentColors)) { changed = true; } - if (TOD::DrawTODSliderRow("Cloud Alpha", settings.clouds[i].cloudAlpha, 0.0f, 1.0f)) { + if (TOD::DrawTODFloatRow("Cloud Alpha", settings.clouds[i].cloudAlpha, settings.inheritFlags[alphaKey], parentAlphas, 0.0f, 1.0f)) { + changed = true; + } + } else { + if (TOD::DrawTODColorRow("Cloud Color", settings.clouds[i].color)) { changed = true; } - TOD::EndTODTable(); + if (TOD::DrawTODFloatRow("Cloud Alpha", settings.clouds[i].cloudAlpha, 0.0f, 1.0f)) { + changed = true; + } } + + TOD::EndTODTable(); } - } - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); + + ImGui::Spacing(); + ImGui::Unindent(10.0f); } } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } } void WeatherWidget::DrawFogSettings() { - bool& doesInherit = settings.inheritance["Fog"]; - ImGui::Checkbox("Inherit From Parent##fog", &doesInherit); - - if (doesInherit && HasParent()) { - settings.fogProperties = GetParent()->settings.fogProperties; - } else { - doesInherit = false; - bool changed = false; - - if (ImGui::BeginTable("FogTable", 3, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingFixedFit)) { - ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthFixed, 250.0f); - ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthFixed, 250.0f); - - // Header row - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TableSetColumnIndex(1); - ImGui::Text("Day"); - ImGui::TableSetColumnIndex(2); - ImGui::Text("Night"); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); - ImGui::TableSetColumnIndex(2); - ImGui::Separator(); + auto editorWindow = EditorWindow::GetSingleton(); + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; - // Near - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Near"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogDayNear", &settings.fogProperties["Day Near"], 0.0f, 50000.0f, "%.0f")) - changed = true; - ImGui::TableSetColumnIndex(2); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogNightNear", &settings.fogProperties["Night Near"], 0.0f, 50000.0f, "%.0f")) - changed = true; + bool changed = false; - // Far - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Far"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogDayFar", &settings.fogProperties["Day Far"], 0.0f, 50000.0f, "%.0f")) - changed = true; - ImGui::TableSetColumnIndex(2); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogNightFar", &settings.fogProperties["Night Far"], 0.0f, 50000.0f, "%.0f")) - changed = true; + if (ImGui::BeginTable("FogTable", 3, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); + ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthFixed, 250.0f); + ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthFixed, 250.0f); + + // Header row + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TableSetColumnIndex(1); + ImGui::Text("Day"); + ImGui::TableSetColumnIndex(2); + ImGui::Text("Night"); + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + ImGui::TableSetColumnIndex(2); + ImGui::Separator(); - // Power - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Power"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogDayPower", &settings.fogProperties["Day Power"], 0.0f, 50000.0f, "%.2f")) - changed = true; - ImGui::TableSetColumnIndex(2); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogNightPower", &settings.fogProperties["Night Power"], 0.0f, 50000.0f, "%.2f")) - changed = true; + // Near + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (hasParent) { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + ImGui::Checkbox("##FogNear", &settings.inheritFlags["Fog_Near"]); + if (settings.inheritFlags["Fog_Near"]) { + settings.fogProperties["Day Near"] = parentWidget->settings.fogProperties["Day Near"]; + settings.fogProperties["Night Near"] = parentWidget->settings.fogProperties["Night Near"]; + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + ImGui::SameLine(); + } + ImGui::Text("Near"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayNear", &settings.fogProperties["Day Near"], 0.0f, 1000000.0f, "%.0f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightNear", &settings.fogProperties["Night Near"], 0.0f, 1000000.0f, "%.0f")) + changed = true; - // Max - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Max"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogDayMax", &settings.fogProperties["Day Max"], 0.0f, 50000.0f, "%.2f")) - changed = true; - ImGui::TableSetColumnIndex(2); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##FogNightMax", &settings.fogProperties["Night Max"], 0.0f, 50000.0f, "%.2f")) - changed = true; + // Far + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (hasParent) { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + ImGui::Checkbox("##FogFar", &settings.inheritFlags["Fog_Far"]); + if (settings.inheritFlags["Fog_Far"]) { + settings.fogProperties["Day Far"] = parentWidget->settings.fogProperties["Day Far"]; + settings.fogProperties["Night Far"] = parentWidget->settings.fogProperties["Night Far"]; + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + ImGui::SameLine(); + } + ImGui::Text("Far"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayFar", &settings.fogProperties["Day Far"], 0.0f, 1000000.0f, "%.0f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightFar", &settings.fogProperties["Night Far"], 0.0f, 1000000.0f, "%.0f")) + changed = true; - ImGui::EndTable(); + // Power + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (hasParent) { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + ImGui::Checkbox("##FogPower", &settings.inheritFlags["Fog_Power"]); + if (settings.inheritFlags["Fog_Power"]) { + settings.fogProperties["Day Power"] = parentWidget->settings.fogProperties["Day Power"]; + settings.fogProperties["Night Power"] = parentWidget->settings.fogProperties["Night Power"]; + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + ImGui::SameLine(); } + ImGui::Text("Power"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayPower", &settings.fogProperties["Day Power"], 0.0f, 10.0f, "%.3f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightPower", &settings.fogProperties["Night Power"], 0.0f, 10.0f, "%.3f")) + changed = true; - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); + // Max + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (hasParent) { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + ImGui::Checkbox("##FogMax", &settings.inheritFlags["Fog_Max"]); + if (settings.inheritFlags["Fog_Max"]) { + settings.fogProperties["Day Max"] = parentWidget->settings.fogProperties["Day Max"]; + settings.fogProperties["Night Max"] = parentWidget->settings.fogProperties["Night Max"]; + } + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + ImGui::SameLine(); } + ImGui::Text("Max"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogDayMax", &settings.fogProperties["Day Max"], 0.0f, 1.0f, "%.3f")) + changed = true; + ImGui::TableSetColumnIndex(2); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##FogNightMax", &settings.fogProperties["Night Max"], 0.0f, 1.0f, "%.3f")) + changed = true; + + ImGui::EndTable(); + } + + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } } void WeatherWidget::DrawProperties(std::string category, std::map properties) { - bool& doesInherit = settings.inheritance[category]; - // Check if any property matches search (only check if search is active) bool hasMatchingProperty = false; if (searchBuffer[0] != '\0') { @@ -782,57 +1197,67 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.enableInheritFromParent && HasParent(); - for (auto& p : properties) { - // Filter individual properties based on search - if (searchBuffer[0] != '\0' && !MatchesSearch(p.first)) { - continue; - } - - // Apply highlight effect if this setting should be highlighted - if (ShouldHighlight(p.first)) { - float elapsed = static_cast(ImGui::GetTime()) - highlightStartTime; - float alpha = 0.3f * (1.0f - std::abs(elapsed - 0.5f) * 2.0f); // Fade in/out over 1 second - ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.6f, 1.0f, alpha)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.7f, 1.0f, alpha)); - } - - switch (p.second) { - case 0: - if (DrawSliderInt8(p.first, settings.weatherProperties[p.first])) changed = true; - break; - case 1: - if (DrawColorEdit(p.first, settings.weatherColors[p.first])) changed = true; - break; - case 2: - if (DrawSliderUint8(p.first, settings.weatherProperties[p.first])) changed = true; - break; - case 3: - if (DrawSliderFloat(p.first, settings.fogProperties[p.first])) changed = true; - break; - default: - break; + for (auto& p : properties) { + // Filter individual properties based on search + if (searchBuffer[0] != '\0' && !MatchesSearch(p.first)) { + continue; + } + + // Apply highlight effect if this setting should be highlighted + if (ShouldHighlight(p.first)) { + float elapsed = static_cast(ImGui::GetTime()) - highlightStartTime; + float alpha = 0.3f * (1.0f - std::abs(elapsed - 0.5f) * 2.0f); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.3f, 0.6f, 1.0f, alpha)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.4f, 0.7f, 1.0f, alpha)); + } + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[p.first]; + if (ImGui::Checkbox(("##inherit_" + p.first).c_str(), &inheritFlag)) { + if (inheritFlag) { + InheritFromParent(p.first); + changed = true; + } } - - if (ShouldHighlight(p.first)) { - ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); } + ImGui::SameLine(); } - - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - ApplyChanges(); + + switch (p.second) { + case 0: + if (WeatherUtils::DrawSliderInt8(p.first, settings.weatherProperties[p.first])) changed = true; + break; + case 1: + if (WeatherUtils::DrawColorEdit(p.first, settings.weatherColors[p.first])) changed = true; + break; + case 2: + if (WeatherUtils::DrawSliderUint8(p.first, settings.weatherProperties[p.first])) changed = true; + break; + case 3: + if (WeatherUtils::DrawSliderFloat(p.first, settings.fogProperties[p.first])) changed = true; + break; + default: + break; + } + + if (ShouldHighlight(p.first)) { + ImGui::PopStyleColor(2); } } + if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); + } + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); @@ -840,11 +1265,95 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.weatherProperties[property]; + settings.weatherProperties[property] = parentWidget->settings.weatherProperties[property]; } else if (settings.weatherColors.find(property) != settings.weatherColors.end()) { - settings.weatherColors[property] = GetParent()->settings.weatherColors[property]; + settings.weatherColors[property] = parentWidget->settings.weatherColors[property]; + } else if (settings.fogProperties.find(property) != settings.fogProperties.end()) { + settings.fogProperties[property] = parentWidget->settings.fogProperties[property]; + } +} + +void WeatherWidget::InheritAllFromParent() +{ + if (!HasParent()) return; + + WeatherWidget* parentWidget = GetParent(); + if (!parentWidget) return; + + // Copy all weather properties + for (const auto& [key, value] : parentWidget->settings.weatherProperties) { + settings.weatherProperties[key] = value; + } + + // Copy all weather colors + for (const auto& [key, value] : parentWidget->settings.weatherColors) { + settings.weatherColors[key] = value; + } + + // Copy all fog properties + for (const auto& [key, value] : parentWidget->settings.fogProperties) { + settings.fogProperties[key] = value; + } + + // Copy atmosphere colors + for (int i = 0; i < ColorTypes::kTotal; i++) { + settings.atmosphereColors[i] = parentWidget->settings.atmosphereColors[i]; + } + + // Copy DALC settings + for (int i = 0; i < ColorTimes::kTotal; i++) { + settings.dalc[i] = parentWidget->settings.dalc[i]; + } + + // Copy cloud settings + for (int i = 0; i < TESWeather::kTotalLayers; i++) { + settings.clouds[i] = parentWidget->settings.clouds[i]; + } + + // Set all inherit flags to true + settings.inheritFlags["DALC_Specular"] = true; + settings.inheritFlags["DALC_Fresnel"] = true; + settings.inheritFlags["DALC_DirXMax"] = true; + settings.inheritFlags["DALC_DirXMin"] = true; + settings.inheritFlags["DALC_DirYMax"] = true; + settings.inheritFlags["DALC_DirYMin"] = true; + settings.inheritFlags["DALC_DirZMax"] = true; + settings.inheritFlags["DALC_DirZMin"] = true; + + settings.inheritFlags["Fog_Near"] = true; + settings.inheritFlags["Fog_Far"] = true; + settings.inheritFlags["Fog_Power"] = true; + settings.inheritFlags["Fog_Max"] = true; + + // Atmosphere colors + static const int displayOrder[] = { 0, 7, 8, 1, 12, 3, 4, 5, 6, 9, 10, 11, 13, 14, 15, 16, 2 }; + for (int idx = 0; idx < ColorTypes::kTotal; idx++) { + int i = displayOrder[idx]; + std::string colorTypeLabel = ColorTypeLabel(i); + settings.inheritFlags["Atmosphere_" + colorTypeLabel] = true; + } + + // Cloud settings + for (int i = 0; i < TESWeather::kTotalLayers; i++) { + settings.inheritFlags[std::format("Cloud{}_Color", i)] = true; + settings.inheritFlags[std::format("Cloud{}_Alpha", i)] = true; + } + + // Apply the changes + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } + + EditorWindow::GetSingleton()->ShowNotification( + std::format("Inherited all settings from {}", parentWidget->GetEditorID()), + ImVec4(0.0f, 1.0f, 0.5f, 1.0f), + 3.0f); } void WeatherWidget::SaveFeatureSettings() @@ -885,13 +1394,11 @@ void WeatherWidget::LoadFeatureSettings() void WeatherWidget::ApplyChanges() { SetWeatherValues(); - logger::info("Applied changes to weather: {}", GetEditorID()); } void WeatherWidget::RevertChanges() { LoadWeatherValues(); - logger::info("Reverted changes for weather: {}", GetEditorID()); } void WeatherWidget::DrawFeatureSettings() @@ -954,256 +1461,6 @@ void WeatherWidget::DrawFeatureSettings() } } -void WeatherWidget::DrawImageSpaceSettings() -{ - if (!weather) { - ImGui::TextColored({ 1.0f, 0.0f, 0.0f, 1.0f }, "Weather object is null!"); - return; - } - - ImGui::TextWrapped("Configure ImageSpace (post-processing) effects for different times of day."); - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - - bool changed = false; - - if (TOD::BeginTODTable("ImageSpace_TOD_Table")) { - TOD::RenderTODHeader(); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); - - // Prepare arrays for TOD rendering - float hdrEyeAdaptSpeed[4]; - float hdrBloomBlurRadius[4]; - float hdrBloomThreshold[4]; - float hdrBloomScale[4]; - float hdrSunlightScale[4]; - float hdrSkyScale[4]; - float cinematicSaturation[4]; - float cinematicBrightness[4]; - float cinematicContrast[4]; - float3 tintColor[4]; - float tintAmount[4]; - float dofStrength[4]; - float dofDistance[4]; - float dofRange[4]; - - for (int i = 0; i < ColorTimes::kTotal; i++) { - hdrEyeAdaptSpeed[i] = settings.imageSpaces[i].hdrEyeAdaptSpeed; - hdrBloomBlurRadius[i] = settings.imageSpaces[i].hdrBloomBlurRadius; - hdrBloomThreshold[i] = settings.imageSpaces[i].hdrBloomThreshold; - hdrBloomScale[i] = settings.imageSpaces[i].hdrBloomScale; - hdrSunlightScale[i] = settings.imageSpaces[i].hdrSunlightScale; - hdrSkyScale[i] = settings.imageSpaces[i].hdrSkyScale; - cinematicSaturation[i] = settings.imageSpaces[i].cinematicSaturation; - cinematicBrightness[i] = settings.imageSpaces[i].cinematicBrightness; - cinematicContrast[i] = settings.imageSpaces[i].cinematicContrast; - tintColor[i] = settings.imageSpaces[i].tintColor; - tintAmount[i] = settings.imageSpaces[i].tintAmount; - dofStrength[i] = settings.imageSpaces[i].dofStrength; - dofDistance[i] = settings.imageSpaces[i].dofDistance; - dofRange[i] = settings.imageSpaces[i].dofRange; - } - - // HDR Settings - if (TOD::DrawTODSliderRow("Eye Adapt Speed", hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].hdrEyeAdaptSpeed = hdrEyeAdaptSpeed[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Bloom Blur Radius", hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].hdrBloomBlurRadius = hdrBloomBlurRadius[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Bloom Threshold", hdrBloomThreshold, 0.0f, 10.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].hdrBloomThreshold = hdrBloomThreshold[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Bloom Scale", hdrBloomScale, 0.0f, 10.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].hdrBloomScale = hdrBloomScale[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Sunlight Scale", hdrSunlightScale, 0.0f, 10.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].hdrSunlightScale = hdrSunlightScale[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Sky Scale", hdrSkyScale, 0.0f, 10.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].hdrSkyScale = hdrSkyScale[i]; - changed = true; - } - - // Separator - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); - - // Cinematic Settings - if (TOD::DrawTODSliderRow("Saturation", cinematicSaturation, 0.0f, 2.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].cinematicSaturation = cinematicSaturation[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Brightness", cinematicBrightness, 0.0f, 2.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].cinematicBrightness = cinematicBrightness[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Contrast", cinematicContrast, 0.0f, 2.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].cinematicContrast = cinematicContrast[i]; - changed = true; - } - - // Separator - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); - - // Tint Settings - if (TOD::DrawTODColorRow("Tint Color", tintColor)) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].tintColor = tintColor[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("Tint Amount", tintAmount, 0.0f, 1.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].tintAmount = tintAmount[i]; - changed = true; - } - - // Separator - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); - - // Depth of Field - if (TOD::DrawTODSliderRow("DOF Strength", dofStrength, 0.0f, 10.0f, "%.3f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].dofStrength = dofStrength[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("DOF Distance", dofDistance, 0.0f, 10000.0f, "%.1f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].dofDistance = dofDistance[i]; - changed = true; - } - - if (TOD::DrawTODSliderRow("DOF Range", dofRange, 0.0f, 10000.0f, "%.1f")) { - for (int i = 0; i < ColorTimes::kTotal; i++) - settings.imageSpaces[i].dofRange = dofRange[i]; - changed = true; - } - - TOD::EndTODTable(); - } - - if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { - SetImageSpaceValues(); - } -} - -void WeatherWidget::LoadImageSpaceValues() -{ - if (!weather) - return; - - for (int timeIdx = 0; timeIdx < ColorTimes::kTotal; timeIdx++) { - RE::TESImageSpace* imageSpace = weather->imageSpaces[timeIdx]; - if (!imageSpace) - continue; - - auto& imgSettings = settings.imageSpaces[timeIdx]; - auto& data = imageSpace->data; - - // HDR - imgSettings.hdrEyeAdaptSpeed = data.hdr.eyeAdaptSpeed; - imgSettings.hdrBloomBlurRadius = data.hdr.bloomBlurRadius; - imgSettings.hdrBloomThreshold = data.hdr.bloomThreshold; - imgSettings.hdrBloomScale = data.hdr.bloomScale; - imgSettings.hdrSunlightScale = data.hdr.sunlightScale; - imgSettings.hdrSkyScale = data.hdr.skyScale; - - // Cinematic - imgSettings.cinematicSaturation = data.cinematic.saturation; - imgSettings.cinematicBrightness = data.cinematic.brightness; - imgSettings.cinematicContrast = data.cinematic.contrast; - - // Tint - imgSettings.tintColor.x = data.tint.color.red; - imgSettings.tintColor.y = data.tint.color.green; - imgSettings.tintColor.z = data.tint.color.blue; - imgSettings.tintAmount = data.tint.amount; - - // Depth of Field - imgSettings.dofStrength = data.depthOfField.strength; - imgSettings.dofDistance = data.depthOfField.distance; - imgSettings.dofRange = data.depthOfField.range; - } -} - -void WeatherWidget::SetImageSpaceValues() -{ - if (!weather) - return; - - for (int timeIdx = 0; timeIdx < ColorTimes::kTotal; timeIdx++) { - RE::TESImageSpace* imageSpace = weather->imageSpaces[timeIdx]; - if (!imageSpace) - continue; - - auto& imgSettings = settings.imageSpaces[timeIdx]; - auto& data = imageSpace->data; - - // HDR - data.hdr.eyeAdaptSpeed = imgSettings.hdrEyeAdaptSpeed; - data.hdr.bloomBlurRadius = imgSettings.hdrBloomBlurRadius; - data.hdr.bloomThreshold = imgSettings.hdrBloomThreshold; - data.hdr.bloomScale = imgSettings.hdrBloomScale; - data.hdr.sunlightScale = imgSettings.hdrSunlightScale; - data.hdr.skyScale = imgSettings.hdrSkyScale; - - // Cinematic - data.cinematic.saturation = imgSettings.cinematicSaturation; - data.cinematic.brightness = imgSettings.cinematicBrightness; - data.cinematic.contrast = imgSettings.cinematicContrast; - - // Tint - data.tint.color.red = imgSettings.tintColor.x; - data.tint.color.green = imgSettings.tintColor.y; - data.tint.color.blue = imgSettings.tintColor.z; - data.tint.amount = imgSettings.tintAmount; - - // Depth of Field - data.depthOfField.strength = imgSettings.dofStrength; - data.depthOfField.distance = imgSettings.dofDistance; - data.depthOfField.range = imgSettings.dofRange; - } -} - void WeatherWidget::UpdateSearchResults() { searchResults.clear(); @@ -1272,18 +1529,6 @@ void WeatherWidget::UpdateSearchResults() searchResults.push_back({ std::format("Cloud {}", layer), "Clouds", layer }); } } - - // Search in ImageSpace settings - std::vector imageSpaceSettings = { - "Eye Adapt Speed", "Bloom Blur Radius", "Bloom Threshold", "Bloom Scale", - "Sunlight Scale", "Sky Scale", "Saturation", "Brightness", "Contrast", - "Tint Color", "Tint Amount", "DOF Strength", "DOF Distance", "DOF Range" - }; - for (const auto& setting : imageSpaceSettings) { - if (ContainsStringIgnoreCase(setting, searchTerm)) { - searchResults.push_back({ setting, "ImageSpace", setting }); - } - } } void WeatherWidget::NavigateToSetting(const SearchResult& result) @@ -1302,3 +1547,35 @@ bool WeatherWidget::ShouldHighlight(const std::string& settingId) const return elapsed < 1.0f; // Highlight for 1 second } +ID3D11ShaderResourceView* WeatherWidget::GetCloudTexture(int layerIndex) +{ + if (cloudTextureCache.contains(layerIndex)) { + return cloudTextureCache[layerIndex]; + } + + const auto& texturePath = settings.clouds[layerIndex].texturePath; + if (texturePath.empty()) { + return nullptr; + } + + // Build resource path for BSA loading: Textures\path (relative to Data folder) + // Note: Skyrim texture paths don't include .dds extension, so we add it + std::string resourcePath = std::string("Textures\\") + texturePath; + + // Add .dds extension if not present + if (resourcePath.size() < 4 || resourcePath.substr(resourcePath.size() - 4) != ".dds") { + resourcePath += ".dds"; + } + + ID3D11ShaderResourceView* srv = nullptr; + ImVec2 textureSize; + + if (Util::LoadDDSTextureFromFile(globals::d3d::device, resourcePath.c_str(), &srv, textureSize)) { + cloudTextureCache[layerIndex] = srv; + return srv; + } + + return nullptr; +} + + diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h index e1413ee86b..f22f8f34c2 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.h +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -15,6 +15,10 @@ class WeatherWidget : public Widget WeatherWidget(TESWeather* a_weather) { + if (!a_weather) { + logger::error("WeatherWidget created with null pointer"); + return; + } form = a_weather; weather = a_weather; LoadWeatherValues(); @@ -46,6 +50,8 @@ class WeatherWidget : public Widget int cloudLayerSpeedX; float3 color[ColorTimes::kTotal]; float cloudAlpha[ColorTimes::kTotal]; + bool enabled = true; + std::string texturePath; }; struct ImageSpaceSettings @@ -76,7 +82,8 @@ class WeatherWidget : public Widget struct Settings { std::string parent = "None"; - std::map inheritance; + // Per-parameter inheritance flags (one per parameter, not per TOD) + std::map inheritFlags; std::map weatherProperties; std::map weatherColors; std::map fogProperties; @@ -96,6 +103,9 @@ class WeatherWidget : public Widget // Cached original vanilla values for restoration Settings vanillaSettings; + // Cloud texture cache (layer index -> SRV) + std::map cloudTextureCache; + ~WeatherWidget(); virtual void DrawWidget() override; @@ -113,10 +123,6 @@ class WeatherWidget : public Widget void SaveFeatureSettings(); void LoadFeatureSettings(); - // ImageSpace methods - void LoadImageSpaceValues(); - void SetImageSpaceValues(); - private: void DrawDALCSettings(); void DrawWeatherColorSettings(); @@ -124,6 +130,9 @@ class WeatherWidget : public Widget void DrawFogSettings(); void DrawFeatureSettings(); + // Cloud texture loading + ID3D11ShaderResourceView* GetCloudTexture(int layerIndex); + // Search functionality struct SearchResult { std::string displayName; @@ -137,7 +146,7 @@ class WeatherWidget : public Widget void UpdateSearchResults(); void NavigateToSetting(const SearchResult& result); bool ShouldHighlight(const std::string& settingId) const; - void DrawImageSpaceSettings(); void DrawProperties(std::string category, std::map properties); void InheritFromParent(const std::string& property); + void InheritAllFromParent(); }; \ No newline at end of file diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index d2112208ee..fc7e88455b 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -1,4 +1,6 @@ #include "WeatherUtils.h" +#include "PaletteWindow.h" +#include "EditorWindow.h" bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring) { @@ -6,7 +8,7 @@ bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string return true; const auto it = std::ranges::search(a_string, a_substring, [](const char a_a, const char a_b) { - return std::tolower(a_a) == std::tolower(a_b); + return std::tolower(static_cast(a_a)) == std::tolower(static_cast(a_b)); }); return !it.empty(); } @@ -23,12 +25,12 @@ float Uint8ToFloat(const uint8_t& value) int8_t FloatToInt8(const float& value) { - return (int8_t)std::lerp(-128, 127, value); + return (int8_t)std::lerp(-128, 127, std::clamp(value, 0.0f, 1.0f)); } uint8_t FloatToUint8(const float& value) { - return (uint8_t)std::lerp(0, 255, value); + return (uint8_t)std::lerp(0, 255, std::clamp(value, 0.0f, 1.0f)); } void Float3ToColor(const float3& f3, RE::Color& color) @@ -142,24 +144,130 @@ std::string ColorTypeLabel(const int i) return label; } -bool DrawSliderInt8(const std::string& label, int& property) +namespace WeatherUtils { - return ImGui::SliderInt(label.c_str(), &property, -128, 127); -} + bool DrawSliderInt8(const std::string& label, int& property) + { + static std::map pendingValues; + static std::map lastChangeTime; + const double debounceDelay = 2.0; + + bool changed = ImGui::SliderInt(label.c_str(), &property, -128, 127); + if (changed) { + pendingValues[label] = property; + lastChangeTime[label] = ImGui::GetTime(); + } + + // Check for any pending values that should be tracked + std::vector toTrack; + for (const auto& [key, changeTime] : lastChangeTime) { + if (ImGui::GetTime() - changeTime >= debounceDelay) { + toTrack.push_back(key); + } + } + + // Track and remove completed entries + for (const auto& key : toTrack) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, static_cast(pendingValues[key])); + pendingValues.erase(key); + lastChangeTime.erase(key); + } + + return changed; + } -bool DrawColorEdit(const std::string& l, float3& property) -{ - return ImGui::ColorEdit3(l.c_str(), (float*)&property); -} + bool DrawColorEdit(const std::string& l, float3& property) + { + static std::map colorCache; + static std::string activeColorId; + static std::map wasPickerOpen; + + std::string cacheId = l; + bool isActive = ImGui::IsPopupOpen(l.c_str(), ImGuiPopupFlags_AnyPopupId); + bool wasActive = wasPickerOpen[cacheId]; + + // Cache the original color when picker is first activated + if (isActive && activeColorId != cacheId) { + colorCache[cacheId] = property; + activeColorId = cacheId; + } else if (!isActive && activeColorId == cacheId) { + activeColorId.clear(); + } + + // Check for Ctrl+Z while picker is active + if (isActive && ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_Z)) { + if (colorCache.contains(cacheId)) { + property = colorCache[cacheId]; + wasPickerOpen[cacheId] = isActive; + return true; + } + } + + bool changed = ImGui::ColorEdit3(l.c_str(), (float*)&property); + + // Track color usage only when picker closes + if (wasActive && !isActive) { + PaletteWindow::GetSingleton()->TrackColorUsage(property); + } + + wasPickerOpen[cacheId] = isActive; + + // Drag-and-drop source + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload("COLOR_DND", &property, sizeof(float3)); + ImGui::ColorButton("##preview", ImVec4(property.x, property.y, property.z, 1.0f), ImGuiColorEditFlags_NoAlpha); + ImGui::EndDragDropSource(); + } + + // Drag-and-drop target + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR_DND")) { + if (payload->DataSize == sizeof(float3)) { + float3 droppedColor = *(const float3*)payload->Data; + property = droppedColor; + changed = true; + } + } + ImGui::EndDragDropTarget(); + } + + return changed; + } -bool DrawSliderUint8(const std::string& label, int& property) -{ - return ImGui::SliderInt(label.c_str(), &property, 0, 255); -} + bool DrawSliderUint8(const std::string& label, int& property) + { + return ImGui::SliderInt(label.c_str(), &property, 0, 255); + } -bool DrawSliderFloat(const std::string& label, float& property) -{ - return ImGui::SliderFloat(label.c_str(), &property, 0, 50000); + bool DrawSliderFloat(const std::string& label, float& property, float min, float max) + { + static std::map pendingValues; + static std::map lastChangeTime; + const double debounceDelay = 2.0; + + bool changed = ImGui::SliderFloat(label.c_str(), &property, min, max); + if (changed) { + pendingValues[label] = property; + lastChangeTime[label] = ImGui::GetTime(); + } + + // Check for any pending values that should be tracked + std::vector toTrack; + for (const auto& [key, changeTime] : lastChangeTime) { + if (ImGui::GetTime() - changeTime >= debounceDelay) { + toTrack.push_back(key); + } + } + + // Track and remove completed entries + for (const auto& key : toTrack) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingValues[key]); + pendingValues.erase(key); + lastChangeTime.erase(key); + } + + return changed; + } } // Time of Day (TOD) helper implementation @@ -274,6 +382,10 @@ namespace TOD bool DrawTODSliderRow(const char* label, float values[4], float minValue, float maxValue, const char* format) { + static std::map pendingValues; + static std::map lastChangeTime; + const double debounceDelay = 2.0; + float factors[4]; GetTimeOfDayFactors(factors); bool changed = false; @@ -294,19 +406,36 @@ namespace TOD if (!isActive) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); - ImGui::PushItemWidth(sliderWidth); - std::string id = std::string("##") + label + std::to_string(i); - if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) - changed = true; - - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); + ImGui::PushItemWidth(sliderWidth); + std::string id = std::string("##") + label + std::to_string(i); + if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { + changed = true; + std::string valueName = std::string(label) + " " + GetPeriodName(i); + pendingValues[valueName] = values[i]; + lastChangeTime[valueName] = ImGui::GetTime(); + } - ImGui::PopItemWidth(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); ImGui::PopItemWidth(); if (!isActive) ImGui::PopStyleVar(); } + + // Check for any pending values that should be tracked + std::vector toTrack; + for (const auto& [key, changeTime] : lastChangeTime) { + if (ImGui::GetTime() - changeTime >= debounceDelay) { + toTrack.push_back(key); + } + } + + // Track and remove completed entries + for (const auto& key : toTrack) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingValues[key]); + pendingValues.erase(key); + lastChangeTime.erase(key); + } return changed; } @@ -319,7 +448,23 @@ namespace TOD ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); + + // Only highlight the title text based on active time of day + bool anyActive = false; + for (int i = 0; i < Count; ++i) { + if (factors[i] > 0.0f) { + anyActive = true; + break; + } + } + if (!anyActive) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + ImGui::Text("%s", label); + + if (!anyActive) + ImGui::PopStyleVar(); + ImGui::TableSetColumnIndex(1); float totalWidth = ImGui::GetContentRegionAvail().x; @@ -334,10 +479,6 @@ namespace TOD if (i > 0) ImGui::SameLine(); - bool isActive = factors[i] > 0.0f; - if (!isActive) - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); - // Create a child region matching the column width to ensure proper alignment ImGui::BeginChild(("##colorcolumn_" + std::string(label) + std::to_string(i)).c_str(), ImVec2(columnWidth, buttonSize), false, ImGuiWindowFlags_NoScrollbar); @@ -350,27 +491,392 @@ namespace TOD std::string id = std::string("##") + label + std::to_string(i); ImVec4 color = ImVec4(colors[i].x, colors[i].y, colors[i].z, 1.0f); - // Use ColorButton with fixed size + static std::map colorCache; + static std::string activeColorId; + + // Use ColorButton with fixed size - no alpha styling on the button itself if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { + colorCache[id] = colors[i]; + activeColorId = id; ImGui::OpenPopup(id.c_str()); } + // Drag-and-drop source + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload("COLOR_DND", &colors[i], sizeof(float3)); + ImGui::ColorButton("##preview", color, ImGuiColorEditFlags_NoAlpha); + ImGui::EndDragDropSource(); + } + + // Drag-and-drop target + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR_DND")) { + if (payload->DataSize == sizeof(float3)) { + float3 droppedColor = *(const float3*)payload->Data; + colors[i] = droppedColor; + changed = true; + } + } + ImGui::EndDragDropTarget(); + } + // Color picker popup - if (ImGui::BeginPopup(id.c_str())) { + static std::map wasPopupOpen; + bool isPopupOpen = ImGui::BeginPopup(id.c_str()); + bool wasOpen = wasPopupOpen[id]; + + if (isPopupOpen) { + // Check for Ctrl+Z while picker is active + if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_Z)) { + if (colorCache.contains(id)) { + colors[i] = colorCache[id]; + changed = true; + } + } + if (ImGui::ColorPicker3((id + "_picker").c_str(), (float*)&colors[i], ImGuiColorEditFlags_NoAlpha)) { changed = true; } ImGui::EndPopup(); + } else if (activeColorId == id) { + activeColorId.clear(); + } + + // Track color usage only when popup closes + if (wasOpen && !isPopupOpen) { + PaletteWindow::GetSingleton()->TrackColorUsage(colors[i]); } + + wasPopupOpen[id] = isPopupOpen; if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); + ImGui::SetTooltip("%s - %.0f%%", GetPeriodName(i), factors[i] * 100.0f); ImGui::EndChild(); + } - if (!isActive) + return changed; + } + + bool DrawTODSliderRow(const char* label, float values[4], bool inheritFlags[4], const float parentValues[4], float minValue, float maxValue, const char* format) + { + static std::map pendingSliderValues; + static std::map sliderLastChangeTime; + const double debounceDelay = 2.0; + + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float checkboxWidth = 20.0f; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float sliderWidth = (totalWidth - (static_cast(Count) - 1) * spacing - (parentValues ? static_cast(Count) * checkboxWidth : 0)) / static_cast(Count); + + for (int i = 0; i < Count; ++i) { + if (i > 0) + ImGui::SameLine(); + + ImGui::BeginGroup(); + + // Per-column inherit checkbox + if (parentValues) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); + ImGui::SetNextItemWidth(checkboxWidth); + std::string inheritId = std::string("##inherit_") + label + std::to_string(i); + if (ImGui::Checkbox(inheritId.c_str(), &inheritFlags[i])) { + if (inheritFlags[i]) { + values[i] = parentValues[i]; + changed = true; + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Inherit from parent"); + ImGui::PopStyleVar(); + ImGui::SameLine(0, 2); + } + + // Slider (disabled if inheriting) + bool isActive = factors[i] > 0.0f; + if (!isActive || (inheritFlags && inheritFlags[i])) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + + if (inheritFlags && inheritFlags[i]) { + values[i] = parentValues[i]; + } + + ImGui::PushItemWidth(sliderWidth); + std::string id = std::string("##") + label + std::to_string(i); + ImGui::BeginDisabled(inheritFlags && inheritFlags[i]); + if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { + changed = true; + if (inheritFlags) + inheritFlags[i] = false; + std::string valueName = std::string(label) + " " + GetPeriodName(i); + pendingSliderValues[valueName] = values[i]; + sliderLastChangeTime[valueName] = ImGui::GetTime(); + } + ImGui::EndDisabled(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); + ImGui::PopItemWidth(); + + if (!isActive || (inheritFlags && inheritFlags[i])) ImGui::PopStyleVar(); + + ImGui::EndGroup(); + } + + // Check for any pending values that should be tracked + std::vector toTrack; + for (const auto& [key, changeTime] : sliderLastChangeTime) { + if (ImGui::GetTime() - changeTime >= debounceDelay) { + toTrack.push_back(key); + } + } + + // Track and remove completed entries + for (const auto& key : toTrack) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingSliderValues[key]); + pendingSliderValues.erase(key); + sliderLastChangeTime.erase(key); + } + + return changed; + } + + bool DrawTODColorRow(const char* label, float3 colors[4], bool& inheritFlag, const float3 parentColors[4]) + { + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + bool anyActive = false; + for (int i = 0; i < Count; ++i) { + if (factors[i] > 0.0f) { + anyActive = true; + break; + } + } + if (!anyActive) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + + // Draw label text + ImGui::Text("%s", label); + + // Draw inherit checkbox right under the label + if (parentColors) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + + std::string inheritId = std::string("##inherit_") + label; + if (ImGui::Checkbox(inheritId.c_str(), &inheritFlag)) { + if (inheritFlag) { + // Copy all parent values + for (int i = 0; i < Count; ++i) { + colors[i] = parentColors[i]; + } + changed = true; + } + // Allow unchecking + } + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Inherit from parent weather"); + } + } + + if (!anyActive) + ImGui::PopStyleVar(); + + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float columnWidth = (totalWidth - 3 * spacing) / 4.0f; + const float buttonSize = ImGui::GetFrameHeight() * 1.5f; + + for (int i = 0; i < Count; ++i) { + if (i > 0) + ImGui::SameLine(); + + ImGui::BeginChild(("##colorcolumn_" + std::string(label) + std::to_string(i)).c_str(), + ImVec2(columnWidth, buttonSize), false, ImGuiWindowFlags_NoScrollbar); + + float centerOffset = (columnWidth - buttonSize) * 0.5f; + if (centerOffset > 0.0f) + ImGui::SetCursorPosX(centerOffset); + + // Apply inherited color if flag is set + if (inheritFlag && parentColors) { + colors[i] = parentColors[i]; + } + + std::string id = std::string("##") + label + std::to_string(i); + ImVec4 color = ImVec4(colors[i].x, colors[i].y, colors[i].z, 1.0f); + + static std::map colorCache; + static std::string activeColorId; + + // Disable editing when inherited + ImGui::BeginDisabled(inheritFlag); + if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { + colorCache[id] = colors[i]; + activeColorId = id; + ImGui::OpenPopup(id.c_str()); + } + + // Drag-and-drop source (only when not inherited) + if (!inheritFlag) { + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload("COLOR_DND", &colors[i], sizeof(float3)); + ImGui::ColorButton("##preview", color, ImGuiColorEditFlags_NoAlpha); + ImGui::EndDragDropSource(); + } + + // Drag-and-drop target + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR_DND")) { + if (payload->DataSize == sizeof(float3)) { + float3 droppedColor = *(const float3*)payload->Data; + colors[i] = droppedColor; + changed = true; + } + } + ImGui::EndDragDropTarget(); + } + } + + // Color picker popup + if (ImGui::BeginPopup(id.c_str())) { + if (colorCache.find(id) == colorCache.end()) { + colorCache[id] = colors[i]; + } + + float3& cachedColor = colorCache[id]; + bool colorChanged = false; + + if (ImGui::ColorPicker3("##picker", &cachedColor.x, ImGuiColorEditFlags_NoAlpha)) { + colors[i] = cachedColor; + colorChanged = true; + changed = true; + } + + ImGui::EndPopup(); + + if (!ImGui::IsPopupOpen(id.c_str()) && activeColorId == id) { + activeColorId = ""; + } + } + ImGui::EndDisabled(); + + ImGui::EndChild(); + } + + return changed; + } + + bool DrawTODFloatRow(const char* label, float values[4], float minValue, float maxValue, const char* format) + { + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float columnWidth = (totalWidth - 3 * spacing) / 4.0f; + + for (int i = 0; i < Count; ++i) { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i); + ImGui::SetNextItemWidth(columnWidth); + if (ImGui::SliderFloat("##value", &values[i], minValue, maxValue, format)) { + changed = true; + } + ImGui::PopID(); + } + + return changed; + } + + bool DrawTODFloatRow(const char* label, float values[4], bool& inheritFlag, const float parentValues[4], float minValue, float maxValue, const char* format) + { + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + ImGui::Text("%s", label); + + // Draw inherit checkbox + if (parentValues) { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); + + std::string inheritId = std::string("##inherit_") + label; + if (ImGui::Checkbox(inheritId.c_str(), &inheritFlag)) { + if (inheritFlag) { + for (int i = 0; i < Count; ++i) { + values[i] = parentValues[i]; + } + changed = true; + } + } + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Inherit from parent weather"); + } + } + + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float columnWidth = (totalWidth - 3 * spacing) / 4.0f; + + ImGui::BeginDisabled(inheritFlag); + for (int i = 0; i < Count; ++i) { + if (i > 0) ImGui::SameLine(); + + // Apply inherited value if flag is set + if (inheritFlag && parentValues) { + values[i] = parentValues[i]; + } + + ImGui::PushID(i); + ImGui::SetNextItemWidth(columnWidth); + if (ImGui::SliderFloat("##value", &values[i], minValue, maxValue, format)) { + changed = true; + } + ImGui::PopID(); } + ImGui::EndDisabled(); return changed; } diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h index 92c4db8c8f..138805ac88 100644 --- a/src/WeatherEditor/WeatherUtils.h +++ b/src/WeatherEditor/WeatherUtils.h @@ -1,6 +1,7 @@ #pragma once #include "Util.h" +#include #include // Case-insensitive substring search helper @@ -15,11 +16,6 @@ void ColorToFloat3(const RE::TESWeather::Data::Color3& color, float3& newColor); std::string ColorTimeLabel(const int i); std::string ColorTypeLabel(const int i); -bool DrawSliderInt8(const std::string& label, int& property); -bool DrawColorEdit(const std::string& l, float3& property); -bool DrawSliderUint8(const std::string& label, int& property); -bool DrawSliderFloat(const std::string& label, float& property); - enum ControlType { INT8_SLIDER = 0, @@ -60,10 +56,14 @@ namespace TOD // Draw a horizontal row of TOD sliders // Returns true if any slider changed bool DrawTODSliderRow(const char* label, float values[4], float minValue = 0.0f, float maxValue = 1.0f, const char* format = "%.2f"); + bool DrawTODSliderRow(const char* label, float values[4], bool inheritFlags[4], const float parentValues[4], float minValue = 0.0f, float maxValue = 1.0f, const char* format = "%.2f"); // Draw a horizontal row of TOD color pickers // Returns true if any color changed bool DrawTODColorRow(const char* label, float3 colors[4]); + bool DrawTODColorRow(const char* label, float3 colors[4], bool& inheritFlag, const float3 parentColors[4]); + bool DrawTODFloatRow(const char* label, float values[4], float minValue = 0.0f, float maxValue = 1.0f, const char* format = "%.2f"); + bool DrawTODFloatRow(const char* label, float values[4], bool& inheritFlag, const float parentValues[4], float minValue = 0.0f, float maxValue = 1.0f, const char* format = "%.2f"); // Draw a horizontal row of TOD int8 sliders // Returns true if any slider changed @@ -79,4 +79,144 @@ namespace TOD // Widget search bar helpers bool BeginWidgetSearchBar(char* searchBuffer, size_t bufferSize, bool& searchActive); -void EndWidgetSearchBar(); \ No newline at end of file +void EndWidgetSearchBar(); + +namespace WeatherUtils +{ + // UI helper functions + bool DrawSliderInt8(const std::string& label, int& property); + bool DrawColorEdit(const std::string& l, float3& property); + bool DrawSliderUint8(const std::string& label, int& property); + bool DrawSliderFloat(const std::string& label, float& property, float min = 0.0f, float max = 50000.0f); + + // Generic form picker combo box using cached widget EditorIDs for performance + // Returns true if selection changed + template + bool DrawFormPickerCached(const char* label, T*& currentForm, const WidgetContainer& widgets, bool showFormID = true, bool allowNone = true, float width = 450.0f) + { + bool changed = false; + + std::string previewText; + if (currentForm) { + // Find the widget for current form + std::string editorID; + for (const auto& widget : widgets) { + if (widget->form == currentForm) { + editorID = widget->GetEditorID(); + break; + } + } + if (editorID.empty()) { + editorID = std::format("{:08X}", currentForm->GetFormID()); + } + + if (showFormID) { + previewText = std::format("{} (0x{:08X})", editorID, currentForm->GetFormID()); + } else { + previewText = editorID; + } + } else { + previewText = "None"; + } + + if (width > 0.0f) { + ImGui::SetNextItemWidth(width); + } + + if (ImGui::BeginCombo(label, previewText.c_str())) { + if (allowNone && ImGui::Selectable("None", currentForm == nullptr)) { + currentForm = nullptr; + changed = true; + } + + for (const auto& widget : widgets) { + if (widget && widget->form) { + T* form = static_cast(widget->form); + std::string editorID = widget->GetEditorID(); + std::string comboLabel; + if (showFormID) { + comboLabel = std::format("{} (0x{:08X})", editorID, form->GetFormID()); + } else { + comboLabel = editorID; + } + + bool isSelected = (currentForm == form); + if (ImGui::Selectable(comboLabel.c_str(), isSelected)) { + currentForm = form; + changed = true; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + } + ImGui::EndCombo(); + } + + return changed; + } + + // Legacy form picker (slow - only use if widgets not available) + template + bool DrawFormPicker(const char* label, T*& currentForm, const Container& formArray, bool showFormID = true, bool allowNone = true, float width = 450.0f) + { + bool changed = false; + + auto GetFormEditorIDSafe = [](T* form) -> std::string { + if (!form) return ""; + + const char* editorID = form->GetFormEditorID(); + if (editorID && editorID[0] != '\0') + return std::string(editorID); + + return std::format("{:08X}", form->GetFormID()); + }; + + std::string previewText; + if (currentForm) { + std::string editorID = GetFormEditorIDSafe(currentForm); + if (showFormID) { + previewText = std::format("{} (0x{:08X})", editorID, currentForm->GetFormID()); + } else { + previewText = editorID; + } + } else { + previewText = "None"; + } + + if (width > 0.0f) { + ImGui::SetNextItemWidth(width); + } + + if (ImGui::BeginCombo(label, previewText.c_str())) { + if (allowNone && ImGui::Selectable("None", currentForm == nullptr)) { + currentForm = nullptr; + changed = true; + } + + for (auto form : formArray) { + if (form) { + std::string editorID = GetFormEditorIDSafe(form); + std::string comboLabel; + if (showFormID) { + comboLabel = std::format("{} (0x{:08X})", editorID, form->GetFormID()); + } else { + comboLabel = editorID; + } + + bool isSelected = (currentForm == form); + if (ImGui::Selectable(comboLabel.c_str(), isSelected)) { + currentForm = form; + changed = true; + } + if (isSelected) { + ImGui::SetItemDefaultFocus(); + } + } + } + ImGui::EndCombo(); + } + + return changed; + } +} \ No newline at end of file diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index 5da2be9b7b..b6c8e91171 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -19,7 +19,6 @@ void Widget::Save() const std::string filePath = std::format("{}\\{}", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName()); const std::string file = std::format("{}\\{}.json", filePath, GetEditorID()); - std::ofstream settingsFile(file); if (!std::filesystem::exists(filePath) || !std::filesystem::is_directory(filePath)) { try { std::filesystem::create_directories(filePath); @@ -29,6 +28,7 @@ void Widget::Save() } } + std::ofstream settingsFile(file); if (!settingsFile.good() || !settingsFile.is_open()) { logger::warn("Failed to open settings file: {}", file); return; @@ -47,10 +47,7 @@ void Widget::Save() settingsFile.close(); return; } - - logger::info("{}: Saving settings file: {}", GetEditorID(), file); - // Write with indentation for readability settingsFile << js.dump(2); settingsFile.flush(); @@ -61,7 +58,6 @@ void Widget::Save() } settingsFile.close(); - logger::info("{}: Successfully saved settings", GetEditorID()); } catch (const nlohmann::json::exception& e) { logger::error("{}: JSON error while saving settings: {}", GetEditorID(), e.what()); @@ -77,8 +73,6 @@ void Widget::Load() std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName(), GetEditorID()); if (!std::filesystem::exists(filePath)) { - // No saved file exists, reset to vanilla/default values - logger::info("{}: No settings file found, resetting to vanilla values", GetEditorID()); js = json(); LoadSettings(); @@ -117,7 +111,6 @@ void Widget::Load() return; } - logger::info("{}: Successfully loaded settings from file", GetEditorID()); LoadSettings(); EditorWindow::GetSingleton()->ShowNotification( @@ -154,15 +147,12 @@ void Widget::Delete() std::string filePath = std::format("{}\\{}\\{}.json", Util::PathHelpers::GetCommunityShaderPath().string(), GetFolderName(), GetEditorID()); if (!std::filesystem::exists(filePath)) { - logger::info("Settings file does not exist, nothing to delete: {}", filePath); return; } try { std::filesystem::remove(filePath); - logger::info("Deleted settings file: {}", filePath); - // Clear the in-memory JSON data js = json(); // Reload settings from vanilla/mod defaults @@ -247,11 +237,21 @@ std::string Widget::GetFolderName() case RE::FormType::Weather: return "Weathers"; case RE::FormType::LightingMaster: - return "LightingTemplates"; + return "Lighting Templates"; case RE::FormType::WorldSpace: return "WorldSpaces"; + case RE::FormType::ImageSpace: + return "ImageSpaces"; + case RE::FormType::VolumetricLighting: + return "Volumetric Lighting"; + case RE::FormType::ShaderParticleGeometryData: + return "Precipitation"; + case RE::FormType::ReferenceEffect: + return "Visual Effects"; + case RE::FormType::Cell: + return "Cell Lighting"; default: - return "Unknown"; + return "Other Editor Widgets"; } } @@ -262,10 +262,11 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s bool useIcons = !editorWindow->settings.useTextButtons && menu && menu->GetSettings().Theme.ShowActionIcons; if (useIcons) { - const float iconSize = ImGui::GetFrameHeight(); + const float iconSize = ImGui::GetFrameHeight() * 0.85f; const ImVec2 buttonSize(iconSize, iconSize); - // Search bar first + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, ImGui::GetStyle().ItemSpacing.y)); + ImGui::SetNextItemWidth(200.0f); if (ImGui::InputTextWithHint(searchId, "Search settings (Ctrl+F)", searchBuffer, sizeof(searchBuffer))) { searchActive = searchBuffer[0] != '\0'; @@ -367,7 +368,7 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s } ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); + ImGui::PopStyleVar(2); } else { // Text button mode const float buttonHeight = ImGui::GetFrameHeight(); diff --git a/src/WeatherEditor/Widget.h b/src/WeatherEditor/Widget.h index 12c9d7bf74..a072db5823 100644 --- a/src/WeatherEditor/Widget.h +++ b/src/WeatherEditor/Widget.h @@ -30,21 +30,81 @@ class Widget virtual std::string GetEditorID() const { - return form->GetFormEditorID(); + // If cachedEditorID looks like a fallback ID, try to get the real one + if (cachedEditorID.find("VolumetricLighting_") == 0 && form) { + const char* editorID = form->GetFormEditorID(); + if (editorID && editorID[0] != '\0') { + const_cast(this)->cachedEditorID = editorID; + return editorID; + } + } + return cachedEditorID; } virtual std::string GetFormID() const { + if (!form) return "00000000"; return std::format("{:08X}", form->GetFormID()); } virtual std::string GetFilename() const { + if (!form) return "Invalid"; if (auto file = form->GetFile()) return std::format("{}", file->GetFilename()); return "Generated"; } + void CacheFormData() + { + if (!form) { + cachedEditorID = "Invalid"; + return; + } + + // Try GetFormEditorID first + const char* editorID = form->GetFormEditorID(); + if (editorID && editorID[0] != '\0') { + cachedEditorID = editorID; + return; + } + + // Search the global EditorID map + auto [map, lock] = RE::TESForm::GetAllFormsByEditorID(); + if (map) { + RE::BSReadLockGuard locker(lock); + for (const auto& [name, f] : *map) { + if (f == form) { + cachedEditorID = std::string(name.c_str()); + return; + } + } + } + + // Fallback to FormID-based names + auto formType = form->GetFormType(); + switch (formType) { + case RE::FormType::ImageSpace: + cachedEditorID = std::format("ImageSpace_{:08X}", form->GetFormID()); + break; + case RE::FormType::VolumetricLighting: + cachedEditorID = std::format("VolumetricLighting_{:08X}", form->GetFormID()); + break; + case RE::FormType::ShaderParticleGeometryData: + cachedEditorID = std::format("ShaderParticleGeometry_{:08X}", form->GetFormID()); + break; + case RE::FormType::LensFlare: + cachedEditorID = std::format("LensFlare_{:08X}", form->GetFormID()); + break; + case RE::FormType::ReferenceEffect: + cachedEditorID = std::format("VisualEffect_{:08X}", form->GetFormID()); + break; + default: + cachedEditorID = std::format("Form_{:08X}", form->GetFormID()); + break; + } + } + virtual void DrawWidget() = 0; bool open = false; @@ -81,6 +141,17 @@ class Widget protected: json js = json(); + std::string cachedEditorID; virtual void DrawMenu(); std::string GetFolderName(); +}; + +// Simple widget for caching form data without full widget functionality +class SimpleFormWidget : public Widget +{ +public: + void DrawWidget() override {} + void LoadSettings() override {} + void SaveSettings() override {} + void ApplyChanges() override {} }; \ No newline at end of file diff --git a/src/WeatherVariableRegistry.h b/src/WeatherVariableRegistry.h index 662fdc3ffb..da6c64637b 100644 --- a/src/WeatherVariableRegistry.h +++ b/src/WeatherVariableRegistry.h @@ -52,8 +52,27 @@ namespace WeatherVariables if (!valuePtr || !lerpFunc) return; - T fromVal = from.is_null() ? defaultValue : from.get(); - T toVal = to.is_null() ? defaultValue : to.get(); + T fromVal = defaultValue; + T toVal = defaultValue; + + if (!from.is_null()) { + try { + fromVal = from.get(); + } catch (const nlohmann::json::type_error& e) { + logger::debug("Type error in Lerp 'from' for {}: {}", name, e.what()); + fromVal = defaultValue; + } + } + + if (!to.is_null()) { + try { + toVal = to.get(); + } catch (const nlohmann::json::type_error& e) { + logger::debug("Type error in Lerp 'to' for {}: {}", name, e.what()); + toVal = defaultValue; + } + } + *valuePtr = lerpFunc(fromVal, toVal, factor); } @@ -67,7 +86,12 @@ namespace WeatherVariables void LoadFromJson(const json& j) override { if (valuePtr && j.contains(name)) { - *valuePtr = j[name].get(); + try { + *valuePtr = j[name].get(); + } catch (const nlohmann::json::type_error& e) { + logger::debug("Type error in LoadFromJson for {}: {}", name, e.what()); + *valuePtr = defaultValue; + } } } From 33ca26b8ce5c6c252ba239a9a0a7f783b3b2e3bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:00:02 +0000 Subject: [PATCH 09/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Deferred.cpp | 1 - src/Features/WeatherEditor.cpp | 1 - src/Utils/UI.cpp | 4 +- src/WeatherEditor/EditorWindow.cpp | 622 ++++++++-------- src/WeatherEditor/EditorWindow.h | 23 +- src/WeatherEditor/PaletteWindow.cpp | 178 ++--- src/WeatherEditor/PaletteWindow.h | 4 +- .../Weather/CellLightingWidget.cpp | 173 +++-- .../Weather/CellLightingWidget.h | 4 +- .../Weather/ImageSpaceWidget.cpp | 1 - src/WeatherEditor/Weather/LensFlareWidget.cpp | 25 +- .../Weather/LightingTemplateWidget.cpp | 90 ++- .../Weather/PrecipitationWidget.cpp | 68 +- .../Weather/ReferenceEffectWidget.cpp | 26 +- .../Weather/VolumetricLightingWidget.cpp | 74 +- src/WeatherEditor/Weather/WeatherWidget.cpp | 669 +++++++++--------- src/WeatherEditor/Weather/WeatherWidget.h | 4 +- src/WeatherEditor/WeatherUtils.cpp | 165 ++--- src/WeatherEditor/WeatherUtils.h | 37 +- src/WeatherEditor/Widget.cpp | 8 +- src/WeatherEditor/Widget.h | 6 +- 21 files changed, 1173 insertions(+), 1010 deletions(-) diff --git a/src/Deferred.cpp b/src/Deferred.cpp index 267e1a12c1..6d59e3791d 100644 --- a/src/Deferred.cpp +++ b/src/Deferred.cpp @@ -418,7 +418,6 @@ void Deferred::DeferredPasses() auto& terrainBlending = globals::features::terrainBlending; auto& ibl = globals::features::ibl; - // Deferred Composite { TracyD3D11Zone(globals::state->tracyCtx, "Deferred Composite"); diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp index 875ea1bc16..5fb517134f 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/WeatherEditor.cpp @@ -56,7 +56,6 @@ void WeatherEditor::DrawSettings() DrawWeatherStatusPanel(); } - void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeather, float currentWeatherPct) { //// Precipitation diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 971bd2f98b..79f42fbee0 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -5,12 +5,11 @@ #ifndef DIRECTINPUT_VERSION # define DIRECTINPUT_VERSION 0x0800 #endif +#include #include #include #include #include -#include -#include #include #include "../Feature.h" @@ -1632,4 +1631,3 @@ namespace Util } } // namespace Util - diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 83e12cc8bd..316e3dfd14 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1,10 +1,10 @@ #include "EditorWindow.h" #include "Features/WeatherEditor.h" +#include "PaletteWindow.h" #include "State.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" -#include "PaletteWindow.h" #include "imgui_internal.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, enableInheritFromParent, editorUIScale, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) @@ -49,7 +49,7 @@ void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool /*filled*/) float r = (i % 2 == 0) ? radius : radius * 0.38f; points[i] = ImVec2(center.x + cosf(angle) * r, center.y + sinf(angle) * r); } - + for (int i = 0; i < 10; i++) { drawList->AddLine(points[i], points[(i + 1) % 10], color, 1.5f); } @@ -154,49 +154,50 @@ void EditorWindow::ShowObjectsWindow() ImGui::Text("Categories"); ImGui::Spacing(); - // List of categories - const char* categories[] = { "Lighting Template", "Weather", "WorldSpace", "Cell Lighting", "ImageSpace", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect" }; - for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { - // Highlight the selected category - if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { - selectedCategory = categories[i]; // Update selected category - } - } // Right column: Objects - ImGui::TableSetColumnIndex(1); - - // Display current active weather - auto sky = globals::game::sky; - if (sky && sky->currentWeather) { - auto currentWeather = sky->currentWeather; - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); - ImGui::Text("Current Active Weather:"); - ImGui::PopStyleColor(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", currentWeather->GetFormEditorID()); - ImGui::SameLine(); - ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); - - // Add button to open the current weather - ImGui::SameLine(); - if (ImGui::SmallButton("Open##CurrentWeather")) { - for (auto& widget : weatherWidgets) { - if (widget->form == currentWeather) { - widget->SetOpen(true); - break; + // List of categories + const char* categories[] = { "Lighting Template", "Weather", "WorldSpace", "Cell Lighting", "ImageSpace", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect" }; + for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { + // Highlight the selected category + if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { + selectedCategory = categories[i]; // Update selected category + } + } // Right column: Objects + ImGui::TableSetColumnIndex(1); + + // Display current active weather + auto sky = globals::game::sky; + if (sky && sky->currentWeather) { + auto currentWeather = sky->currentWeather; + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); + ImGui::Text("Current Active Weather:"); + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", currentWeather->GetFormEditorID()); + ImGui::SameLine(); + ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); + + // Add button to open the current weather + ImGui::SameLine(); + if (ImGui::SmallButton("Open##CurrentWeather")) { + for (auto& widget : weatherWidgets) { + if (widget->form == currentWeather) { + widget->SetOpen(true); + break; + } } } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - } - // Handle Ctrl+F to focus search bar - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { - if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { - ImGui::SetKeyboardFocusHere(); + // Handle Ctrl+F to focus search bar + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(); + } } - } ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); + ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); ImGui::SameLine(); HelpMarker("Type a part of an object name to filter the list.\nCtrl+F: Focus search\nEnter: Open selected"); @@ -227,7 +228,8 @@ void EditorWindow::ShowObjectsWindow() ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Recent:"); ImGui::SameLine(); for (size_t i = 0; i < std::min(size_t(5), recentIt->second.size()); ++i) { - if (i > 0) ImGui::SameLine(); + if (i > 0) + ImGui::SameLine(); if (ImGui::SmallButton(recentIt->second[i].c_str())) { // Find and open widget in current category's collection auto& widgets = selectedCategory == "Weather" ? weatherWidgets : @@ -239,7 +241,7 @@ void EditorWindow::ShowObjectsWindow() selectedCategory == "Lens Flare" ? lensFlareWidgets : selectedCategory == "Visual Effect" ? referenceEffectWidgets : weatherWidgets; - + for (auto& widget : widgets) { if (widget->GetEditorID() == recentIt->second[i]) { widget->SetOpen(true); @@ -274,27 +276,27 @@ void EditorWindow::ShowObjectsWindow() } } - // Display objects based on the selected category - std::vector> emptyWidgets; - const auto& widgets = selectedCategory == "Weather" ? weatherWidgets : - selectedCategory == "WorldSpace" ? worldSpaceWidgets : - selectedCategory == "Cell Lighting" ? emptyWidgets : - selectedCategory == "ImageSpace" ? imageSpaceWidgets : - selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : - selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : - selectedCategory == "Lens Flare" ? lensFlareWidgets : - selectedCategory == "Visual Effect" ? referenceEffectWidgets : - lightingTemplateWidgets; - // Sort widgets based on current sort column - std::vector sortedWidgets; - sortedWidgets.reserve(widgets.size()); - for (const auto& w : widgets) { - sortedWidgets.push_back(w.get()); - } - if (currentSortColumn != SortColumn::None) { - std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { - int comparison = 0; - switch (currentSortColumn) { + // Display objects based on the selected category + std::vector> emptyWidgets; + const auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + selectedCategory == "Cell Lighting" ? emptyWidgets : + selectedCategory == "ImageSpace" ? imageSpaceWidgets : + selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : + selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : + selectedCategory == "Lens Flare" ? lensFlareWidgets : + selectedCategory == "Visual Effect" ? referenceEffectWidgets : + lightingTemplateWidgets; + // Sort widgets based on current sort column + std::vector sortedWidgets; + sortedWidgets.reserve(widgets.size()); + for (const auto& w : widgets) { + sortedWidgets.push_back(w.get()); + } + if (currentSortColumn != SortColumn::None) { + std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { + int comparison = 0; + switch (currentSortColumn) { case SortColumn::EditorID: comparison = _stricmp(a->GetEditorID().c_str(), b->GetEditorID().c_str()); break; @@ -315,94 +317,85 @@ void EditorWindow::ShowObjectsWindow() } default: break; - } - return sortAscending ? (comparison < 0) : (comparison > 0); - }); - } + } + return sortAscending ? (comparison < 0) : (comparison > 0); + }); + } - // Special handling for Cell Lighting category - if (selectedCategory == "Cell Lighting") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto cell = player->parentCell; - bool isInterior = cell->IsInteriorCell(); - - if (isInterior) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - - // No favorite star for cell lighting (it's always the current cell) - ImGui::Dummy(ImVec2(24, 24)); - - ImGui::TableNextColumn(); - - // Display current cell name - const char* cellName = cell->GetName(); - std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; - std::string label = std::format("[CURRENT CELL] {}", displayName); - - bool isOpen = currentCellLightingWidget && currentCellLightingWidget->IsOpen(); - if (ImGui::Selectable(label.c_str(), isOpen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - // Open or reuse the cell lighting widget + // Special handling for Cell Lighting category + if (selectedCategory == "Cell Lighting") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto cell = player->parentCell; + bool isInterior = cell->IsInteriorCell(); + + if (isInterior) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + // No favorite star for cell lighting (it's always the current cell) + ImGui::Dummy(ImVec2(24, 24)); + + ImGui::TableNextColumn(); + + // Display current cell name + const char* cellName = cell->GetName(); + std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; + std::string label = std::format("[CURRENT CELL] {}", displayName); + + bool isOpen = currentCellLightingWidget && currentCellLightingWidget->IsOpen(); + if (ImGui::Selectable(label.c_str(), isOpen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + // Open or reuse the cell lighting widget + if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { + currentCellLightingWidget->SetOpen(true); + } else { + currentCellLightingWidget = std::make_unique(cell); + currentCellLightingWidget->CacheFormData(); + currentCellLightingWidget->Load(); + currentCellLightingWidget->SetOpen(true); + } + } + } + + // Highlight current cell + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + + // Enter key to open + if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { currentCellLightingWidget->SetOpen(true); - } else { - currentCellLightingWidget = std::make_unique(cell); - currentCellLightingWidget->CacheFormData(); - currentCellLightingWidget->Load(); - currentCellLightingWidget->SetOpen(true); } } - } - - // Highlight current cell - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - - // Enter key to open - if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { - currentCellLightingWidget->SetOpen(true); + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", cell->GetFormID()); + + // File column + ImGui::TableNextColumn(); + auto file = cell->GetFile(0); + if (file) { + ImGui::Text("%s", file->fileName); } + + // Status column + ImGui::TableNextColumn(); + ImGui::Text("Interior Cell"); + } else { + // Show message that cell lighting is only for interior cells + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "Cell Lighting is only available for interior cells."); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "You are currently in an exterior cell."); } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text("0x%08X", cell->GetFormID()); - - // File column - ImGui::TableNextColumn(); - auto file = cell->GetFile(0); - if (file) { - ImGui::Text("%s", file->fileName); - } - - // Status column - ImGui::TableNextColumn(); - ImGui::Text("Interior Cell"); } else { - // Show message that cell lighting is only for interior cells + // No player or cell ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "Cell Lighting is only available for interior cells."); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "You are currently in an exterior cell."); + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Player cell not available."); } - } else { - // No player or cell - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Player cell not available."); - } - } - - // Get current cell's lighting template for prioritization - RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; - if (selectedCategory == "Lighting Template") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto& cellData = player->parentCell->GetRuntimeData(); - currentCellLightingTemplate = cellData.lightingTemplate; } // Get current cell's lighting template for prioritization @@ -413,14 +406,107 @@ void EditorWindow::ShowObjectsWindow() auto& cellData = player->parentCell->GetRuntimeData(); currentCellLightingTemplate = cellData.lightingTemplate; } - } - // Filtered display of widgets - show current cell's lighting template first - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + // Get current cell's lighting template for prioritization + RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; + if (selectedCategory == "Lighting Template") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto& cellData = player->parentCell->GetRuntimeData(); + currentCellLightingTemplate = cellData.lightingTemplate; + } + } + + // Filtered display of widgets - show current cell's lighting template first + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + for (int i = 0; i < sortedWidgets.size(); ++i) { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) + continue; + + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + continue; + + // Apply quick filters + if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) + continue; + if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) + continue; + + auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); + auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); + ImGui::TableNextRow(); + + // Highlight current cell's lighting template + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + + ImGui::TableSetColumnIndex(0); + + // Favorite star + if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + ToggleFavorite(sortedWidgets[i]->GetEditorID()); + } + + ImGui::TableNextColumn(); + + // Editor ID column with [CURRENT] prefix + bool isSelected = sortedWidgets[i]->IsOpen(); + if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); + } + } + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID()); + } + + // Context menu + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; + + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; + Save(); + } + } + + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(sortedWidgets[i]->GetEditorID()); + Save(); + } + + ImGui::EndPopup(); + } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); + + // File column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); + + // Status column + ImGui::TableNextColumn(); + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text("%s", markedRecord->second.c_str()); + } + } + } + + // Filtered display of widgets - regular list for (int i = 0; i < sortedWidgets.size(); ++i) { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) - continue; + // Skip current cell's lighting template if already shown + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) + continue; + } if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) continue; @@ -431,50 +517,84 @@ void EditorWindow::ShowObjectsWindow() if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) continue; - auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); - auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); + auto editorLabel = sortedWidgets[i]->GetEditorID(); + auto markedRecord = settings.markedRecords.find(editorLabel); ImGui::TableNextRow(); - // Highlight current cell's lighting template - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + // Set background colour + if (markedRecord != settings.markedRecords.end()) { + auto& color = settings.recordMarkers[markedRecord->second]; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); + } ImGui::TableSetColumnIndex(0); // Favorite star - if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { ToggleFavorite(sortedWidgets[i]->GetEditorID()); } ImGui::TableNextColumn(); - // Editor ID column with [CURRENT] prefix + // Editor ID column bool isSelected = sortedWidgets[i]->IsOpen(); if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } } + + // Show ImageSpace and VolumetricLighting info for weather widgets + if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { + auto* weatherWidget = dynamic_cast(sortedWidgets[i]); + if (weatherWidget && weatherWidget->weather) { + ImGui::BeginTooltip(); + + // ImageSpace info + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "ImageSpace:"); + for (int tod = 0; tod < 4; tod++) { + auto imgSpace = weatherWidget->weather->imageSpaces[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + imgSpace ? imgSpace->GetFormEditorID() : "None"); + } + + ImGui::Spacing(); + + // VolumetricLighting info + ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Volumetric Lighting:"); + for (int tod = 0; tod < 4; tod++) { + auto volLight = weatherWidget->weather->volumetricLighting[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + volLight ? volLight->GetFormEditorID() : "None"); + } + + ImGui::EndTooltip(); + } + } + // Enter key to open if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } - // Context menu + // Opens a context menu on right click to mark records by color if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; for (auto& recordMarker : settings.recordMarkers) { if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; + settings.markedRecords[editorLabel] = recordMarker.first; Save(); } } if (ImGui::MenuItem("Remove")) { - markedRecords.erase(sortedWidgets[i]->GetEditorID()); + markedRecords.erase(editorLabel); Save(); } @@ -491,6 +611,9 @@ void EditorWindow::ShowObjectsWindow() // Status column ImGui::TableNextColumn(); + + // Re-check if the record exists after potential removal + markedRecord = settings.markedRecords.find(editorLabel); if (markedRecord != settings.markedRecords.end()) { ImGui::Text("%s", markedRecord->second.c_str()); } @@ -543,37 +666,6 @@ void EditorWindow::ShowObjectsWindow() AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } } - - // Show ImageSpace and VolumetricLighting info for weather widgets - if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { - auto* weatherWidget = dynamic_cast(sortedWidgets[i]); - if (weatherWidget && weatherWidget->weather) { - ImGui::BeginTooltip(); - - // ImageSpace info - ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "ImageSpace:"); - for (int tod = 0; tod < 4; tod++) { - auto imgSpace = weatherWidget->weather->imageSpaces[tod]; - ImGui::Text(" %s: %s", - TOD::GetPeriodName(tod), - imgSpace ? imgSpace->GetFormEditorID() : "None"); - } - - ImGui::Spacing(); - - // VolumetricLighting info - ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Volumetric Lighting:"); - for (int tod = 0; tod < 4; tod++) { - auto volLight = weatherWidget->weather->volumetricLighting[tod]; - ImGui::Text(" %s: %s", - TOD::GetPeriodName(tod), - volLight ? volLight->GetFormEditorID() : "None"); - } - - ImGui::EndTooltip(); - } - } - // Enter key to open if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { sortedWidgets[i]->SetOpen(true); @@ -616,96 +708,7 @@ void EditorWindow::ShowObjectsWindow() ImGui::Text("%s", markedRecord->second.c_str()); } } - } - - // Filtered display of widgets - regular list - for (int i = 0; i < sortedWidgets.size(); ++i) { - // Skip current cell's lighting template if already shown - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) - continue; - } - - if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) - continue; - - // Apply quick filters - if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) - continue; - if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) - continue; - - auto editorLabel = sortedWidgets[i]->GetEditorID(); - auto markedRecord = settings.markedRecords.find(editorLabel); - ImGui::TableNextRow(); - - // Set background colour - if (markedRecord != settings.markedRecords.end()) { - auto& color = settings.recordMarkers[markedRecord->second]; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); - } - - ImGui::TableSetColumnIndex(0); - - // Favorite star - if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { - ToggleFavorite(sortedWidgets[i]->GetEditorID()); - } - - ImGui::TableNextColumn(); - - // Editor ID column - bool isSelected = sortedWidgets[i]->IsOpen(); - if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - } - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - - // Opens a context menu on right click to mark records by color - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { - auto& markedRecords = settings.markedRecords; - - for (auto& recordMarker : settings.recordMarkers) { - if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[editorLabel] = recordMarker.first; - Save(); - } - } - - if (ImGui::MenuItem("Remove")) { - markedRecords.erase(editorLabel); - Save(); - } - - ImGui::EndPopup(); - } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); - - // File column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); - - // Status column - ImGui::TableNextColumn(); - - // Re-check if the record exists after potential removal - markedRecord = settings.markedRecords.find(editorLabel); - if (markedRecord != settings.markedRecords.end()) { - ImGui::Text("%s", markedRecord->second.c_str()); - } - } ImGui::EndTable(); + ImGui::EndTable(); } ImGui::EndTable(); @@ -971,7 +974,7 @@ void EditorWindow::RenderUI() settingsSelectedCategory = "Flags"; } ImGui::Separator(); - + // Current cell lighting auto player = RE::PlayerCharacter::GetSingleton(); if (player && player->parentCell && player->parentCell->IsInteriorCell()) { @@ -982,7 +985,7 @@ void EditorWindow::RenderUI() currentCellLightingWidget->SetOpen(true); found = true; } - + if (!found) { // Create new widget for current cell currentCellLightingWidget = std::make_unique(player->parentCell); @@ -999,9 +1002,9 @@ void EditorWindow::RenderUI() ImGui::SetTooltip("Only available in interior cells"); } } - + ImGui::Separator(); - + if (ImGui::Checkbox("Auto-Apply Changes", &settings.autoApplyChanges)) { Save(); } @@ -1026,7 +1029,7 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Palette", nullptr, PaletteWindow::GetSingleton()->open)) { PaletteWindow::GetSingleton()->open = !PaletteWindow::GetSingleton()->open; } - + ImGui::Separator(); ImGui::Text("Open Widgets:"); ImGui::Separator(); @@ -1096,7 +1099,7 @@ void EditorWindow::RenderUI() ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Favorites: %d", (int)settings.favoriteWidgets.size()); - + // Count total recent widgets across all categories int totalRecent = 0; for (const auto& [category, widgets] : settings.recentWidgets) { @@ -1156,21 +1159,22 @@ void EditorWindow::RenderUI() ImGui::Text(" [TIME PAUSED]"); ImGui::PopStyleColor(); } - - // Close button on the right side - float menuBarHeight = ImGui::GetFrameHeight(); - float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar - ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); - if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { - open = false; - } - ImGui::PopStyleColor(3); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Close Weather Editor (Esc)"); - } ImGui::EndMainMenuBar(); + + // Close button on the right side + float menuBarHeight = ImGui::GetFrameHeight(); + float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar + ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { + open = false; + } + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Close Weather Editor (Esc)"); + } + ImGui::EndMainMenuBar(); } auto width = ImGui::GetIO().DisplaySize.x; @@ -1192,7 +1196,7 @@ void EditorWindow::RenderUI() } ShowWidgetWindow(); - + // Show palette window PaletteWindow::GetSingleton()->Draw(); @@ -1201,7 +1205,7 @@ void EditorWindow::RenderUI() // Pop the alpha style var ImGui::PopStyleVar(); - + // Restore previous font scale io.FontGlobalScale = previousScale; } @@ -1436,26 +1440,26 @@ void EditorWindow::ShowSettingsWindow() ImGui::Checkbox("Use text buttons instead of icons", &settings.useTextButtons); AddTooltip("Display action buttons as text labels instead of icons"); - + ImGui::Checkbox("Enable 'Inherit From Parent' feature", &settings.enableInheritFromParent); AddTooltip("Show checkboxes to copy settings from parent weather (editor-only feature)"); - + ImGui::Separator(); ImGui::TextUnformatted("UI Scale"); ImGui::Spacing(); - + if (ImGui::SliderFloat("Editor UI Scale", &settings.editorUIScale, 0.5f, 2.0f, "%.2f")) { Save(); } AddTooltip("Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)"); - + if (ImGui::Button("Reset to 1.0")) { settings.editorUIScale = 1.0f; Save(); } ImGui::SameLine(); AddTooltip("Reset UI scale to default (100%)"); - + ImGui::Separator(); ImGui::TextUnformatted("Session & History"); ImGui::Spacing(); @@ -1475,7 +1479,7 @@ void EditorWindow::ShowSettingsWindow() settings.favoriteWidgets.clear(); Save(); } - + } else if (settingsSelectedCategory == "Flags") { if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); @@ -1784,7 +1788,7 @@ void EditorWindow::RenderNotifications() void EditorWindow::AddToRecent(const std::string& widgetId, const std::string& category) { auto& categoryRecent = settings.recentWidgets[category]; - + // Remove if already exists auto it = std::find(categoryRecent.begin(), categoryRecent.end(), widgetId); if (it != categoryRecent.end()) { diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index 38040fe32c..3a17bf82ce 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -101,32 +101,35 @@ class EditorWindow { "Complete", { 0.0f, 130.0f / 255.0f, 0.0f, 1.0f } } }; std::map markedRecords; - bool autoApplyChanges = true; - bool suppressDeleteWarning = false; - bool useTextButtons = false; - bool enableInheritFromParent = false; - float editorUIScale = 1.0f; + bool autoApplyChanges = true; + bool suppressDeleteWarning = false; + bool useTextButtons = false; + bool enableInheritFromParent = false; + float editorUIScale = 1.0f; std::vector favoriteWidgets; std::map> recentWidgets; int maxRecentWidgets = 10; bool rememberOpenWidgets = true; std::vector lastOpenWidgets; - + // Palette settings - struct PaletteColorEntry { + struct PaletteColorEntry + { float r, g, b; int useCount = 0; float lastUsedTime = 0.0f; bool isFavorite = false; }; - struct PaletteValueEntry { + struct PaletteValueEntry + { std::string name; float value; int useCount = 0; float lastUsedTime = 0.0f; bool isFavorite = false; }; - struct PaletteFavoriteColor { + struct PaletteFavoriteColor + { bool hasValue = false; float r = 0.0f, g = 0.0f, b = 0.0f; }; @@ -156,7 +159,7 @@ class EditorWindow std::string settingsFilename = "EditorSettings"; bool showSettingsWindow = false; std::string settingsSelectedCategory = "Flags"; - + // Sorting state enum class SortColumn { diff --git a/src/WeatherEditor/PaletteWindow.cpp b/src/WeatherEditor/PaletteWindow.cpp index 2d1aa1c38a..e5a054867a 100644 --- a/src/WeatherEditor/PaletteWindow.cpp +++ b/src/WeatherEditor/PaletteWindow.cpp @@ -38,31 +38,31 @@ void PaletteWindow::DrawColorsTab() ImGui::SeparatorText("Favourites"); ImGui::TextWrapped("Drag colours here to save as favourites."); ImGui::Spacing(); - + for (int i = 0; i < maxFavoriteSlots; i++) { if (i > 0) ImGui::SameLine(0.0f, spacing); - + std::string id = "##favorite_" + std::to_string(i); - + if (favoriteColors[i].has_value()) { // Show filled favorite slot auto& color = favoriteColors[i].value(); ImVec4 colorVec(color.x, color.y, color.z, 1.0f); - + if (ImGui::ColorButton(id.c_str(), colorVec, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { copiedColor = color; hasColorInClipboard = true; ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", color.x, color.y, color.z).c_str()); } - + // Drag source if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("COLOR_DND", &color, sizeof(float3)); ImGui::ColorButton("##preview", colorVec, ImGuiColorEditFlags_NoAlpha); ImGui::EndDragDropSource(); } - + // Right-click to clear if (ImGui::BeginPopupContextItem()) { if (ImGui::Selectable("Clear favourite")) { @@ -71,7 +71,7 @@ void PaletteWindow::DrawColorsTab() } ImGui::EndPopup(); } - + if (ImGui::IsItemHovered()) { ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nClick to copy\nRight-click to clear", color.x, color.y, color.z); } @@ -81,21 +81,21 @@ void PaletteWindow::DrawColorsTab() ImGui::PushStyleColor(ImGuiCol_Button, emptyColor); ImGui::Button(id.c_str(), ImVec2(buttonSize, buttonSize)); ImGui::PopStyleColor(); - + // Draw star icon in center ImVec2 buttonMin = ImGui::GetItemRectMin(); ImVec2 buttonMax = ImGui::GetItemRectMax(); ImVec2 center = ImVec2((buttonMin.x + buttonMax.x) * 0.5f, (buttonMin.y + buttonMax.y) * 0.5f); float starSize = buttonSize * 0.4f; ImU32 starColor = IM_COL32(160, 160, 160, 255); - + DrawIconStar(center, starSize, starColor, false); - + if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Drag a colour here to add to favourites"); } } - + // Drag-and-drop target if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR_DND")) { @@ -113,7 +113,7 @@ void PaletteWindow::DrawColorsTab() // Recently Used section ImGui::SeparatorText("Recently Used"); auto recentColors = GetRecentColors(5); - + if (recentColors.empty()) { ImGui::TextDisabled("No recent colors"); } else { @@ -139,7 +139,7 @@ void PaletteWindow::DrawColorsTab() } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nUsed %d times\nClick to copy", + ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nUsed %d times\nClick to copy", entry->color.x, entry->color.y, entry->color.z, entry->useCount); } } @@ -154,48 +154,48 @@ void PaletteWindow::DrawColorsTab() ImGui::TextWrapped("Favourite/most commonly used colours here."); ImGui::Spacing(); - auto mostUsedColors = GetMostUsedColors(20); - - if (mostUsedColors.empty()) { - ImGui::TextDisabled("No frequently used colors yet"); - ImGui::TextDisabled("(Colors used 3+ times will appear here)"); - } else { - int colorIndex = 0; - for (auto* entry : mostUsedColors) { - if (colorIndex > 0 && colorIndex % 10 != 0) - ImGui::SameLine(0.0f, spacing); - - ImVec4 color(entry->color.x, entry->color.y, entry->color.z, 1.0f); - std::string id = "##mostused_color_" + std::to_string(colorIndex); - - if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { - copiedColor = entry->color; - hasColorInClipboard = true; - ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", entry->color.x, entry->color.y, entry->color.z).c_str()); - } + auto mostUsedColors = GetMostUsedColors(20); - // Drag source - if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { - ImGui::SetDragDropPayload("COLOR_DND", &entry->color, sizeof(float3)); - ImGui::ColorButton("##preview", color, ImGuiColorEditFlags_NoAlpha); - ImGui::EndDragDropSource(); - } + if (mostUsedColors.empty()) { + ImGui::TextDisabled("No frequently used colors yet"); + ImGui::TextDisabled("(Colors used 3+ times will appear here)"); + } else { + int colorIndex = 0; + for (auto* entry : mostUsedColors) { + if (colorIndex > 0 && colorIndex % 10 != 0) + ImGui::SameLine(0.0f, spacing); + + ImVec4 color(entry->color.x, entry->color.y, entry->color.z, 1.0f); + std::string id = "##mostused_color_" + std::to_string(colorIndex); + + if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { + copiedColor = entry->color; + hasColorInClipboard = true; + ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", entry->color.x, entry->color.y, entry->color.z).c_str()); + } + + // Drag source + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { + ImGui::SetDragDropPayload("COLOR_DND", &entry->color, sizeof(float3)); + ImGui::ColorButton("##preview", color, ImGuiColorEditFlags_NoAlpha); + ImGui::EndDragDropSource(); + } - // Right-click to remove - if (ImGui::BeginPopupContextItem()) { - if (ImGui::Selectable("Remove from palette")) { - auto it = std::find_if(colorEntries.begin(), colorEntries.end(), - [entry](const ColorEntry& e) { return &e == entry; }); - if (it != colorEntries.end()) { - colorEntries.erase(it); - Save(); - } + // Right-click to remove + if (ImGui::BeginPopupContextItem()) { + if (ImGui::Selectable("Remove from palette")) { + auto it = std::find_if(colorEntries.begin(), colorEntries.end(), + [entry](const ColorEntry& e) { return &e == entry; }); + if (it != colorEntries.end()) { + colorEntries.erase(it); + Save(); } - ImGui::EndPopup(); } + ImGui::EndPopup(); + } if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nUsed %d times\nClick to copy\nRight-click to remove", + ImGui::SetTooltip("RGB: %.3f, %.3f, %.3f\nUsed %d times\nClick to copy\nRight-click to remove", entry->color.x, entry->color.y, entry->color.z, entry->useCount); } @@ -235,46 +235,46 @@ void PaletteWindow::DrawValuesTab() ImGui::TextWrapped("Favourite/most commonly used values here."); ImGui::Spacing(); - auto mostUsedValues = GetMostUsedValues(20); - - if (mostUsedValues.empty()) { - ImGui::TextDisabled("No frequently used values yet"); - ImGui::TextDisabled("(Values used 3+ times will appear here)"); - } else { - for (auto* entry : mostUsedValues) { - std::string label = std::format("{}: {:.3f}##{}", entry->name, entry->value, (void*)entry); - if (ImGui::Selectable(label.c_str())) { - copiedValue = entry->value; - copiedValueName = entry->name; - hasValueInClipboard = true; - ImGui::SetClipboardText(std::to_string(entry->value).c_str()); - } + auto mostUsedValues = GetMostUsedValues(20); + + if (mostUsedValues.empty()) { + ImGui::TextDisabled("No frequently used values yet"); + ImGui::TextDisabled("(Values used 3+ times will appear here)"); + } else { + for (auto* entry : mostUsedValues) { + std::string label = std::format("{}: {:.3f}##{}", entry->name, entry->value, (void*)entry); + if (ImGui::Selectable(label.c_str())) { + copiedValue = entry->value; + copiedValueName = entry->name; + hasValueInClipboard = true; + ImGui::SetClipboardText(std::to_string(entry->value).c_str()); + } - // Right-click to remove - if (ImGui::BeginPopupContextItem()) { - if (ImGui::Selectable("Remove from palette")) { - auto it = std::find_if(valueEntries.begin(), valueEntries.end(), - [entry](const ValueEntry& e) { return &e == entry; }); - if (it != valueEntries.end()) { - valueEntries.erase(it); - Save(); - } + // Right-click to remove + if (ImGui::BeginPopupContextItem()) { + if (ImGui::Selectable("Remove from palette")) { + auto it = std::find_if(valueEntries.begin(), valueEntries.end(), + [entry](const ValueEntry& e) { return &e == entry; }); + if (it != valueEntries.end()) { + valueEntries.erase(it); + Save(); } - ImGui::EndPopup(); } + ImGui::EndPopup(); + } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Used %d times\nClick to copy\nRight-click to remove", entry->useCount); - } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Used %d times\nClick to copy\nRight-click to remove", entry->useCount); } } } +} std::vector PaletteWindow::GetRecentColors(int count) { std::vector result; std::vector allColors; - + for (auto& entry : colorEntries) { allColors.push_back(&entry); } @@ -293,7 +293,7 @@ std::vector PaletteWindow::GetRecentColors(int count std::vector PaletteWindow::GetMostUsedColors(int count) { std::vector result; - + for (auto& entry : colorEntries) { if (entry.useCount >= 3 || entry.isFavorite) { result.push_back(&entry); @@ -319,7 +319,7 @@ std::vector PaletteWindow::GetRecentValues(int count { std::vector result; std::vector allValues; - + for (auto& entry : valueEntries) { allValues.push_back(&entry); } @@ -338,7 +338,7 @@ std::vector PaletteWindow::GetRecentValues(int count std::vector PaletteWindow::GetMostUsedValues(int count) { std::vector result; - + for (auto& entry : valueEntries) { if (entry.useCount >= 3 || entry.isFavorite) { result.push_back(&entry); @@ -363,7 +363,7 @@ std::vector PaletteWindow::GetMostUsedValues(int cou void PaletteWindow::TrackColorUsage(const float3& color) { float currentTime = static_cast(ImGui::GetTime()); - + // Find existing entry (with small epsilon for float comparison) const float epsilon = 0.001f; for (auto& entry : colorEntries) { @@ -389,7 +389,7 @@ void PaletteWindow::TrackColorUsage(const float3& color) void PaletteWindow::TrackValueUsage(const std::string& name, float value) { float currentTime = static_cast(ImGui::GetTime()); - + // Find existing entry const float epsilon = 0.001f; for (auto& entry : valueEntries) { @@ -414,7 +414,7 @@ void PaletteWindow::TrackValueUsage(const std::string& name, float value) void PaletteWindow::Save() { auto editorWindow = EditorWindow::GetSingleton(); - + // Save favorites editorWindow->settings.paletteFavorites = {}; for (size_t i = 0; i < favoriteColors.size(); i++) { @@ -427,7 +427,7 @@ void PaletteWindow::Save() editorWindow->settings.paletteFavorites[i].hasValue = false; } } - + // Save color entries editorWindow->settings.paletteColors.clear(); for (const auto& entry : colorEntries) { @@ -440,7 +440,7 @@ void PaletteWindow::Save() e.isFavorite = entry.isFavorite; editorWindow->settings.paletteColors.push_back(e); } - + // Save value entries editorWindow->settings.paletteValues.clear(); for (const auto& entry : valueEntries) { @@ -452,14 +452,14 @@ void PaletteWindow::Save() e.isFavorite = entry.isFavorite; editorWindow->settings.paletteValues.push_back(e); } - + editorWindow->Save(); } void PaletteWindow::Load() { auto editorWindow = EditorWindow::GetSingleton(); - + // Load favorites for (size_t i = 0; i < editorWindow->settings.paletteFavorites.size(); i++) { const auto& fav = editorWindow->settings.paletteFavorites[i]; @@ -469,7 +469,7 @@ void PaletteWindow::Load() favoriteColors[i].reset(); } } - + // Load color entries colorEntries.clear(); for (const auto& e : editorWindow->settings.paletteColors) { @@ -480,7 +480,7 @@ void PaletteWindow::Load() entry.isFavorite = e.isFavorite; colorEntries.push_back(entry); } - + // Load value entries valueEntries.clear(); for (const auto& e : editorWindow->settings.paletteValues) { diff --git a/src/WeatherEditor/PaletteWindow.h b/src/WeatherEditor/PaletteWindow.h index bc7eb96a5c..e3b311eb9d 100644 --- a/src/WeatherEditor/PaletteWindow.h +++ b/src/WeatherEditor/PaletteWindow.h @@ -39,11 +39,11 @@ class PaletteWindow private: std::vector colorEntries; std::vector valueEntries; - + // Favorites - fixed slots that users can drag colors into static constexpr int maxFavoriteSlots = 10; std::array, maxFavoriteSlots> favoriteColors; - + // Clipboard float3 copiedColor = { 0.0f, 0.0f, 0.0f }; float copiedValue = 0.0f; diff --git a/src/WeatherEditor/Weather/CellLightingWidget.cpp b/src/WeatherEditor/Weather/CellLightingWidget.cpp index 198fdc4cc0..545443a61c 100644 --- a/src/WeatherEditor/Weather/CellLightingWidget.cpp +++ b/src/WeatherEditor/Weather/CellLightingWidget.cpp @@ -27,49 +27,69 @@ void CellLightingWidget::DrawWidget() if (ImGui::BeginTabBar("CellLightingTabs")) { if (ImGui::BeginTabItem("Colors")) { ImGui::SeparatorText("Ambient & Directional"); - if (WeatherUtils::DrawColorEdit("Ambient Color", settings.ambient)) changed = true; - if (WeatherUtils::DrawColorEdit("Directional Color", settings.directional)) changed = true; - if (WeatherUtils::DrawSliderFloat("Directional Fade", settings.directionalFade, 0.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawColorEdit("Ambient Color", settings.ambient)) + changed = true; + if (WeatherUtils::DrawColorEdit("Directional Color", settings.directional)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Directional Fade", settings.directionalFade, 0.0f, 1.0f)) + changed = true; ImGui::SeparatorText("Fog Colors"); - if (WeatherUtils::DrawColorEdit("Fog Near Color", settings.fogColorNear)) changed = true; - if (WeatherUtils::DrawColorEdit("Fog Far Color", settings.fogColorFar)) changed = true; + if (WeatherUtils::DrawColorEdit("Fog Near Color", settings.fogColorNear)) + changed = true; + if (WeatherUtils::DrawColorEdit("Fog Far Color", settings.fogColorFar)) + changed = true; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Fog")) { ImGui::SeparatorText("Fog Distance"); - if (WeatherUtils::DrawSliderFloat("Fog Near", settings.fogNear, 0.0f, 10000.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Fog Far", settings.fogFar, 0.0f, 50000.0f)) changed = true; - + if (WeatherUtils::DrawSliderFloat("Fog Near", settings.fogNear, 0.0f, 10000.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Fog Far", settings.fogFar, 0.0f, 50000.0f)) + changed = true; + ImGui::SeparatorText("Fog Properties"); - if (WeatherUtils::DrawSliderFloat("Fog Power", settings.fogPower, 0.0f, 10.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Fog Clamp (Max)", settings.fogClamp, 0.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Fog Power", settings.fogPower, 0.0f, 10.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Fog Clamp (Max)", settings.fogClamp, 0.0f, 1.0f)) + changed = true; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Directional Ambient")) { ImGui::SeparatorText("Directional Ambient Lighting (DALC)"); - - if (WeatherUtils::DrawColorEdit("X+ (Right)", settings.directionalXPlus)) changed = true; - if (WeatherUtils::DrawColorEdit("X- (Left)", settings.directionalXMinus)) changed = true; - if (WeatherUtils::DrawColorEdit("Y+ (Front)", settings.directionalYPlus)) changed = true; - if (WeatherUtils::DrawColorEdit("Y- (Back)", settings.directionalYMinus)) changed = true; - if (WeatherUtils::DrawColorEdit("Z+ (Up)", settings.directionalZPlus)) changed = true; - if (WeatherUtils::DrawColorEdit("Z- (Down)", settings.directionalZMinus)) changed = true; - if (WeatherUtils::DrawColorEdit("Specular", settings.directionalSpecular)) changed = true; - if (WeatherUtils::DrawSliderFloat("Fresnel Power", settings.fresnelPower, 0.0f, 10.0f)) changed = true; + + if (WeatherUtils::DrawColorEdit("X+ (Right)", settings.directionalXPlus)) + changed = true; + if (WeatherUtils::DrawColorEdit("X- (Left)", settings.directionalXMinus)) + changed = true; + if (WeatherUtils::DrawColorEdit("Y+ (Front)", settings.directionalYPlus)) + changed = true; + if (WeatherUtils::DrawColorEdit("Y- (Back)", settings.directionalYMinus)) + changed = true; + if (WeatherUtils::DrawColorEdit("Z+ (Up)", settings.directionalZPlus)) + changed = true; + if (WeatherUtils::DrawColorEdit("Z- (Down)", settings.directionalZMinus)) + changed = true; + if (WeatherUtils::DrawColorEdit("Specular", settings.directionalSpecular)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Fresnel Power", settings.fresnelPower, 0.0f, 10.0f)) + changed = true; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Advanced")) { ImGui::SeparatorText("Light Fade Distances"); - if (WeatherUtils::DrawSliderFloat("Light Fade Start", settings.lightFadeStart, 0.0f, 10000.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Light Fade End", settings.lightFadeEnd, 0.0f, 20000.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Clip Distance", settings.clipDist, 0.0f, 50000.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Light Fade Start", settings.lightFadeStart, 0.0f, 10000.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Light Fade End", settings.lightFadeEnd, 0.0f, 20000.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Clip Distance", settings.clipDist, 0.0f, 50000.0f)) + changed = true; ImGui::SeparatorText("Directional Rotation"); int xyDegrees = settings.directionalXY; @@ -90,17 +110,28 @@ void CellLightingWidget::DrawWidget() ImGui::TextWrapped("These flags control which lighting properties are inherited from the cell's lighting template."); ImGui::Separator(); - if (ImGui::Checkbox("Inherit Ambient Color", &settings.inheritAmbientColor)) changed = true; - if (ImGui::Checkbox("Inherit Directional Color", &settings.inheritDirectionalColor)) changed = true; - if (ImGui::Checkbox("Inherit Fog Color", &settings.inheritFogColor)) changed = true; - if (ImGui::Checkbox("Inherit Fog Near", &settings.inheritFogNear)) changed = true; - if (ImGui::Checkbox("Inherit Fog Far", &settings.inheritFogFar)) changed = true; - if (ImGui::Checkbox("Inherit Directional Rotation", &settings.inheritDirectionalRotation)) changed = true; - if (ImGui::Checkbox("Inherit Directional Fade", &settings.inheritDirectionalFade)) changed = true; - if (ImGui::Checkbox("Inherit Clip Distance", &settings.inheritClipDistance)) changed = true; - if (ImGui::Checkbox("Inherit Fog Power", &settings.inheritFogPower)) changed = true; - if (ImGui::Checkbox("Inherit Fog Max (Clamp)", &settings.inheritFogMax)) changed = true; - if (ImGui::Checkbox("Inherit Light Fade Distances", &settings.inheritLightFadeDistances)) changed = true; + if (ImGui::Checkbox("Inherit Ambient Color", &settings.inheritAmbientColor)) + changed = true; + if (ImGui::Checkbox("Inherit Directional Color", &settings.inheritDirectionalColor)) + changed = true; + if (ImGui::Checkbox("Inherit Fog Color", &settings.inheritFogColor)) + changed = true; + if (ImGui::Checkbox("Inherit Fog Near", &settings.inheritFogNear)) + changed = true; + if (ImGui::Checkbox("Inherit Fog Far", &settings.inheritFogFar)) + changed = true; + if (ImGui::Checkbox("Inherit Directional Rotation", &settings.inheritDirectionalRotation)) + changed = true; + if (ImGui::Checkbox("Inherit Directional Fade", &settings.inheritDirectionalFade)) + changed = true; + if (ImGui::Checkbox("Inherit Clip Distance", &settings.inheritClipDistance)) + changed = true; + if (ImGui::Checkbox("Inherit Fog Power", &settings.inheritFogPower)) + changed = true; + if (ImGui::Checkbox("Inherit Fog Max (Clamp)", &settings.inheritFogMax)) + changed = true; + if (ImGui::Checkbox("Inherit Light Fade Distances", &settings.inheritLightFadeDistances)) + changed = true; ImGui::EndTabItem(); } @@ -111,7 +142,7 @@ void CellLightingWidget::DrawWidget() if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } - + ImGui::End(); } } @@ -152,17 +183,27 @@ void CellLightingWidget::LoadSettings() settings.fogColorFar = { arr[0], arr[1], arr[2] }; } } - if (js.contains("fogNear")) settings.fogNear = js["fogNear"]; - if (js.contains("fogFar")) settings.fogFar = js["fogFar"]; - if (js.contains("fogPower")) settings.fogPower = js["fogPower"]; - if (js.contains("fogClamp")) settings.fogClamp = js["fogClamp"]; - if (js.contains("directionalFade")) settings.directionalFade = js["directionalFade"]; - if (js.contains("clipDist")) settings.clipDist = js["clipDist"]; - if (js.contains("lightFadeStart")) settings.lightFadeStart = js["lightFadeStart"]; - if (js.contains("lightFadeEnd")) settings.lightFadeEnd = js["lightFadeEnd"]; - if (js.contains("directionalXY")) settings.directionalXY = js["directionalXY"]; - if (js.contains("directionalZ")) settings.directionalZ = js["directionalZ"]; - + if (js.contains("fogNear")) + settings.fogNear = js["fogNear"]; + if (js.contains("fogFar")) + settings.fogFar = js["fogFar"]; + if (js.contains("fogPower")) + settings.fogPower = js["fogPower"]; + if (js.contains("fogClamp")) + settings.fogClamp = js["fogClamp"]; + if (js.contains("directionalFade")) + settings.directionalFade = js["directionalFade"]; + if (js.contains("clipDist")) + settings.clipDist = js["clipDist"]; + if (js.contains("lightFadeStart")) + settings.lightFadeStart = js["lightFadeStart"]; + if (js.contains("lightFadeEnd")) + settings.lightFadeEnd = js["lightFadeEnd"]; + if (js.contains("directionalXY")) + settings.directionalXY = js["directionalXY"]; + if (js.contains("directionalZ")) + settings.directionalZ = js["directionalZ"]; + if (js.contains("dalc")) { auto& dalc = js["dalc"]; if (dalc.contains("xPlus") && dalc["xPlus"].is_array() && dalc["xPlus"].size() == 3) { @@ -186,24 +227,36 @@ void CellLightingWidget::LoadSettings() if (dalc.contains("specular") && dalc["specular"].is_array() && dalc["specular"].size() == 3) { settings.directionalSpecular = { dalc["specular"][0], dalc["specular"][1], dalc["specular"][2] }; } - if (dalc.contains("fresnelPower")) settings.fresnelPower = dalc["fresnelPower"]; + if (dalc.contains("fresnelPower")) + settings.fresnelPower = dalc["fresnelPower"]; } - + if (js.contains("inherit")) { auto& inherit = js["inherit"]; - if (inherit.contains("ambientColor")) settings.inheritAmbientColor = inherit["ambientColor"]; - if (inherit.contains("directionalColor")) settings.inheritDirectionalColor = inherit["directionalColor"]; - if (inherit.contains("fogColor")) settings.inheritFogColor = inherit["fogColor"]; - if (inherit.contains("fogNear")) settings.inheritFogNear = inherit["fogNear"]; - if (inherit.contains("fogFar")) settings.inheritFogFar = inherit["fogFar"]; - if (inherit.contains("directionalRotation")) settings.inheritDirectionalRotation = inherit["directionalRotation"]; - if (inherit.contains("directionalFade")) settings.inheritDirectionalFade = inherit["directionalFade"]; - if (inherit.contains("clipDistance")) settings.inheritClipDistance = inherit["clipDistance"]; - if (inherit.contains("fogPower")) settings.inheritFogPower = inherit["fogPower"]; - if (inherit.contains("fogMax")) settings.inheritFogMax = inherit["fogMax"]; - if (inherit.contains("lightFadeDistances")) settings.inheritLightFadeDistances = inherit["lightFadeDistances"]; + if (inherit.contains("ambientColor")) + settings.inheritAmbientColor = inherit["ambientColor"]; + if (inherit.contains("directionalColor")) + settings.inheritDirectionalColor = inherit["directionalColor"]; + if (inherit.contains("fogColor")) + settings.inheritFogColor = inherit["fogColor"]; + if (inherit.contains("fogNear")) + settings.inheritFogNear = inherit["fogNear"]; + if (inherit.contains("fogFar")) + settings.inheritFogFar = inherit["fogFar"]; + if (inherit.contains("directionalRotation")) + settings.inheritDirectionalRotation = inherit["directionalRotation"]; + if (inherit.contains("directionalFade")) + settings.inheritDirectionalFade = inherit["directionalFade"]; + if (inherit.contains("clipDistance")) + settings.inheritClipDistance = inherit["clipDistance"]; + if (inherit.contains("fogPower")) + settings.inheritFogPower = inherit["fogPower"]; + if (inherit.contains("fogMax")) + settings.inheritFogMax = inherit["fogMax"]; + if (inherit.contains("lightFadeDistances")) + settings.inheritLightFadeDistances = inherit["lightFadeDistances"]; } - + } catch (const std::exception& e) { logger::error("CellLighting {}: Failed to load from JSON: {}", GetEditorID(), e.what()); // Fall through to load from form @@ -270,7 +323,7 @@ void CellLightingWidget::SaveSettings() js["lightFadeEnd"] = settings.lightFadeEnd; js["directionalXY"] = settings.directionalXY; js["directionalZ"] = settings.directionalZ; - + js["dalc"]["xPlus"] = { settings.directionalXPlus.x, settings.directionalXPlus.y, settings.directionalXPlus.z }; js["dalc"]["xMinus"] = { settings.directionalXMinus.x, settings.directionalXMinus.y, settings.directionalXMinus.z }; js["dalc"]["yPlus"] = { settings.directionalYPlus.x, settings.directionalYPlus.y, settings.directionalYPlus.z }; diff --git a/src/WeatherEditor/Weather/CellLightingWidget.h b/src/WeatherEditor/Weather/CellLightingWidget.h index 567049c539..377103e1b7 100644 --- a/src/WeatherEditor/Weather/CellLightingWidget.h +++ b/src/WeatherEditor/Weather/CellLightingWidget.h @@ -40,7 +40,7 @@ class CellLightingWidget : public Widget float lightFadeEnd = 5000.0f; uint32_t directionalXY = 0; uint32_t directionalZ = 0; - + // Directional ambient lighting colors (DALC equivalent) float3 directionalXPlus = { 1.0f, 1.0f, 1.0f }; float3 directionalXMinus = { 1.0f, 1.0f, 1.0f }; @@ -50,7 +50,7 @@ class CellLightingWidget : public Widget float3 directionalZMinus = { 1.0f, 1.0f, 1.0f }; float3 directionalSpecular = { 1.0f, 1.0f, 1.0f }; float fresnelPower = 1.0f; - + // Inheritance flags bool inheritAmbientColor = false; bool inheritDirectionalColor = false; diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp index 0ef4a82420..430d63c26e 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -33,7 +33,6 @@ void ImageSpaceWidget::DrawWidget() ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { - // Draw header with search and Save/Load/Delete buttons DrawWidgetHeader("##ImageSpaceSearch", false, true); diff --git a/src/WeatherEditor/Weather/LensFlareWidget.cpp b/src/WeatherEditor/Weather/LensFlareWidget.cpp index 284dc8c805..8815cf21c4 100644 --- a/src/WeatherEditor/Weather/LensFlareWidget.cpp +++ b/src/WeatherEditor/Weather/LensFlareWidget.cpp @@ -11,10 +11,12 @@ void LensFlareWidget::DrawWidget() bool changed = false; ImGui::SeparatorText("Fade Distance"); - if (ImGui::SliderFloat("Fade Dist Radius Scale", &settings.fadeDistRadiusScale, 0.0f, 10.0f)) changed = true; + if (ImGui::SliderFloat("Fade Dist Radius Scale", &settings.fadeDistRadiusScale, 0.0f, 10.0f)) + changed = true; ImGui::SeparatorText("Color"); - if (ImGui::SliderFloat("Color Influence", &settings.colorInfluence, 0.0f, 1.0f)) changed = true; + if (ImGui::SliderFloat("Color Influence", &settings.colorInfluence, 0.0f, 1.0f)) + changed = true; if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); @@ -30,15 +32,18 @@ void LensFlareWidget::LoadSettings() if (!js.empty()) { try { - if (js.contains("fadeDistRadiusScale")) settings.fadeDistRadiusScale = js["fadeDistRadiusScale"]; - if (js.contains("colorInfluence")) settings.colorInfluence = js["colorInfluence"]; - } catch (const std::exception& e) { - logger::error("LensFlare {}: Failed to load from JSON: {}", GetEditorID(), e.what()); + if (js.contains("fadeDistRadiusScale")) + settings.fadeDistRadiusScale = js["fadeDistRadiusScale"]; + if (js.contains("colorInfluence")) + settings.colorInfluence = js["colorInfluence"]; + } catch (const std::exception& e) { + logger::error("LensFlare {}: Failed to load from JSON: {}", GetEditorID(), e.what()); + } + } else { + settings.fadeDistRadiusScale = lensFlare->fadeDistRadiusScale; + settings.colorInfluence = lensFlare->colorInfluence; } -} else { - settings.fadeDistRadiusScale = lensFlare->fadeDistRadiusScale; - settings.colorInfluence = lensFlare->colorInfluence; -} originalSettings = settings; + originalSettings = settings; } void LensFlareWidget::SaveSettings() diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index 67dd818564..a6c3b68734 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -61,34 +61,50 @@ void LightingTemplateWidget::DrawBasicSettings() if (ImGui::CollapsingHeader("Ambient & Directional", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Ambient Color") && WeatherUtils::DrawColorEdit("Ambient Color", settings.ambient)) changed = true; - if (MatchesSearch("Ambient Color")) ImGui::Spacing(); - if (MatchesSearch("Directional Color") && WeatherUtils::DrawColorEdit("Directional Color", settings.directional)) changed = true; - if (MatchesSearch("Directional Color")) ImGui::Spacing(); + if (MatchesSearch("Ambient Color") && WeatherUtils::DrawColorEdit("Ambient Color", settings.ambient)) + changed = true; + if (MatchesSearch("Ambient Color")) + ImGui::Spacing(); + if (MatchesSearch("Directional Color") && WeatherUtils::DrawColorEdit("Directional Color", settings.directional)) + changed = true; + if (MatchesSearch("Directional Color")) + ImGui::Spacing(); } if (ImGui::CollapsingHeader("Directional Settings", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Directional XY") && WeatherUtils::DrawSliderFloat("Directional XY", settings.directionalXY)) changed = true; - if (MatchesSearch("Directional XY")) ImGui::Spacing(); - if (MatchesSearch("Directional Z") && WeatherUtils::DrawSliderFloat("Directional Z", settings.directionalZ)) changed = true; - if (MatchesSearch("Directional Z")) ImGui::Spacing(); - if (MatchesSearch("Directional Fade") && WeatherUtils::DrawSliderFloat("Directional Fade", settings.directionalFade)) changed = true; - if (MatchesSearch("Directional Fade")) ImGui::Spacing(); + if (MatchesSearch("Directional XY") && WeatherUtils::DrawSliderFloat("Directional XY", settings.directionalXY)) + changed = true; + if (MatchesSearch("Directional XY")) + ImGui::Spacing(); + if (MatchesSearch("Directional Z") && WeatherUtils::DrawSliderFloat("Directional Z", settings.directionalZ)) + changed = true; + if (MatchesSearch("Directional Z")) + ImGui::Spacing(); + if (MatchesSearch("Directional Fade") && WeatherUtils::DrawSliderFloat("Directional Fade", settings.directionalFade)) + changed = true; + if (MatchesSearch("Directional Fade")) + ImGui::Spacing(); } if (ImGui::CollapsingHeader("Light Fade", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Light Fade Start") && WeatherUtils::DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) changed = true; - if (MatchesSearch("Light Fade Start")) ImGui::Spacing(); - if (MatchesSearch("Light Fade End") && WeatherUtils::DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) changed = true; - if (MatchesSearch("Light Fade End")) ImGui::Spacing(); + if (MatchesSearch("Light Fade Start") && WeatherUtils::DrawSliderFloat("Light Fade Start", settings.lightFadeStart)) + changed = true; + if (MatchesSearch("Light Fade Start")) + ImGui::Spacing(); + if (MatchesSearch("Light Fade End") && WeatherUtils::DrawSliderFloat("Light Fade End", settings.lightFadeEnd)) + changed = true; + if (MatchesSearch("Light Fade End")) + ImGui::Spacing(); } if (ImGui::CollapsingHeader("Other", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (MatchesSearch("Clip Distance") && WeatherUtils::DrawSliderFloat("Clip Distance", settings.clipDist)) changed = true; - if (MatchesSearch("Clip Distance")) ImGui::Spacing(); + if (MatchesSearch("Clip Distance") && WeatherUtils::DrawSliderFloat("Clip Distance", settings.clipDist)) + changed = true; + if (MatchesSearch("Clip Distance")) + ImGui::Spacing(); } if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { @@ -101,22 +117,34 @@ void LightingTemplateWidget::DrawFogSettings() bool changed = false; ImGui::Spacing(); - if (MatchesSearch("Fog Color Near") && WeatherUtils::DrawColorEdit("Fog Color Near", settings.fogColorNear)) changed = true; - if (MatchesSearch("Fog Color Near")) ImGui::Spacing(); - if (MatchesSearch("Fog Color Far") && WeatherUtils::DrawColorEdit("Fog Color Far", settings.fogColorFar)) changed = true; - if (MatchesSearch("Fog Color Far")) ImGui::Spacing(); + if (MatchesSearch("Fog Color Near") && WeatherUtils::DrawColorEdit("Fog Color Near", settings.fogColorNear)) + changed = true; + if (MatchesSearch("Fog Color Near")) + ImGui::Spacing(); + if (MatchesSearch("Fog Color Far") && WeatherUtils::DrawColorEdit("Fog Color Far", settings.fogColorFar)) + changed = true; + if (MatchesSearch("Fog Color Far")) + ImGui::Spacing(); ImGui::Spacing(); - if (MatchesSearch("Fog Near") && WeatherUtils::DrawSliderFloat("Fog Near", settings.fogNear)) changed = true; - if (MatchesSearch("Fog Near")) ImGui::Spacing(); - if (MatchesSearch("Fog Far") && WeatherUtils::DrawSliderFloat("Fog Far", settings.fogFar)) changed = true; - if (MatchesSearch("Fog Far")) ImGui::Spacing(); + if (MatchesSearch("Fog Near") && WeatherUtils::DrawSliderFloat("Fog Near", settings.fogNear)) + changed = true; + if (MatchesSearch("Fog Near")) + ImGui::Spacing(); + if (MatchesSearch("Fog Far") && WeatherUtils::DrawSliderFloat("Fog Far", settings.fogFar)) + changed = true; + if (MatchesSearch("Fog Far")) + ImGui::Spacing(); ImGui::Spacing(); - if (MatchesSearch("Fog Power") && WeatherUtils::DrawSliderFloat("Fog Power", settings.fogPower)) changed = true; - if (MatchesSearch("Fog Power")) ImGui::Spacing(); - if (MatchesSearch("Fog Clamp") && WeatherUtils::DrawSliderFloat("Fog Clamp", settings.fogClamp)) changed = true; - if (MatchesSearch("Fog Clamp")) ImGui::Spacing(); + if (MatchesSearch("Fog Power") && WeatherUtils::DrawSliderFloat("Fog Power", settings.fogPower)) + changed = true; + if (MatchesSearch("Fog Power")) + ImGui::Spacing(); + if (MatchesSearch("Fog Clamp") && WeatherUtils::DrawSliderFloat("Fog Clamp", settings.fogClamp)) + changed = true; + if (MatchesSearch("Fog Clamp")) + ImGui::Spacing(); if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); @@ -129,9 +157,11 @@ void LightingTemplateWidget::DrawDALCSettings() if (ImGui::CollapsingHeader("Basic DALC", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Spacing(); - if (WeatherUtils::DrawColorEdit("Specular", settings.dalc.specular)) changed = true; + if (WeatherUtils::DrawColorEdit("Specular", settings.dalc.specular)) + changed = true; ImGui::Spacing(); - if (WeatherUtils::DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) changed = true; + if (WeatherUtils::DrawSliderFloat("Fresnel Power", settings.dalc.fresnelPower)) + changed = true; ImGui::Spacing(); } diff --git a/src/WeatherEditor/Weather/PrecipitationWidget.cpp b/src/WeatherEditor/Weather/PrecipitationWidget.cpp index 7b0ca2ae2f..7a4bf19435 100644 --- a/src/WeatherEditor/Weather/PrecipitationWidget.cpp +++ b/src/WeatherEditor/Weather/PrecipitationWidget.cpp @@ -21,25 +21,34 @@ void PrecipitationWidget::DrawWidget() } ImGui::SeparatorText("Particle Size"); - if (WeatherUtils::DrawSliderFloat("Size X", settings.particleSizeX, 0.0f, 10.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Size Y", settings.particleSizeY, 0.0f, 10.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Size X", settings.particleSizeX, 0.0f, 10.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Size Y", settings.particleSizeY, 0.0f, 10.0f)) + changed = true; ImGui::SeparatorText("Velocity"); - if (WeatherUtils::DrawSliderFloat("Gravity Velocity", settings.gravityVelocity, -100.0f, 100.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Rotation Velocity", settings.rotationVelocity, -360.0f, 360.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Gravity Velocity", settings.gravityVelocity, -100.0f, 100.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Rotation Velocity", settings.rotationVelocity, -360.0f, 360.0f)) + changed = true; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Position")) { ImGui::SeparatorText("Offset"); - if (WeatherUtils::DrawSliderFloat("Center Offset Min", settings.centerOffsetMin, -1000.0f, 1000.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Center Offset Max", settings.centerOffsetMax, -1000.0f, 1000.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Start Rotation Range", settings.startRotationRange, 0.0f, 360.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Center Offset Min", settings.centerOffsetMin, -1000.0f, 1000.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Center Offset Max", settings.centerOffsetMax, -1000.0f, 1000.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Start Rotation Range", settings.startRotationRange, 0.0f, 360.0f)) + changed = true; ImGui::SeparatorText("Volume"); - if (WeatherUtils::DrawSliderFloat("Box Size", settings.boxSize, 0.0f, 10000.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Particle Density", settings.particleDensity, 0.0f, 10.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Box Size", settings.boxSize, 0.0f, 10000.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Particle Density", settings.particleDensity, 0.0f, 10.0f)) + changed = true; ImGui::EndTabItem(); } @@ -85,25 +94,38 @@ void PrecipitationWidget::LoadSettings() if (!js.empty()) { try { - if (js.contains("gravityVelocity")) settings.gravityVelocity = js["gravityVelocity"]; - if (js.contains("rotationVelocity")) settings.rotationVelocity = js["rotationVelocity"]; - if (js.contains("particleSizeX")) settings.particleSizeX = js["particleSizeX"]; - if (js.contains("particleSizeY")) settings.particleSizeY = js["particleSizeY"]; - if (js.contains("centerOffsetMin")) settings.centerOffsetMin = js["centerOffsetMin"]; - if (js.contains("centerOffsetMax")) settings.centerOffsetMax = js["centerOffsetMax"]; - if (js.contains("startRotationRange")) settings.startRotationRange = js["startRotationRange"]; - if (js.contains("numSubtexturesX")) settings.numSubtexturesX = js["numSubtexturesX"]; - if (js.contains("numSubtexturesY")) settings.numSubtexturesY = js["numSubtexturesY"]; - if (js.contains("particleType")) settings.particleType = js["particleType"]; - if (js.contains("boxSize")) settings.boxSize = js["boxSize"]; - if (js.contains("particleDensity")) settings.particleDensity = js["particleDensity"]; - if (js.contains("particleTexture")) settings.particleTexture = js["particleTexture"].get(); + if (js.contains("gravityVelocity")) + settings.gravityVelocity = js["gravityVelocity"]; + if (js.contains("rotationVelocity")) + settings.rotationVelocity = js["rotationVelocity"]; + if (js.contains("particleSizeX")) + settings.particleSizeX = js["particleSizeX"]; + if (js.contains("particleSizeY")) + settings.particleSizeY = js["particleSizeY"]; + if (js.contains("centerOffsetMin")) + settings.centerOffsetMin = js["centerOffsetMin"]; + if (js.contains("centerOffsetMax")) + settings.centerOffsetMax = js["centerOffsetMax"]; + if (js.contains("startRotationRange")) + settings.startRotationRange = js["startRotationRange"]; + if (js.contains("numSubtexturesX")) + settings.numSubtexturesX = js["numSubtexturesX"]; + if (js.contains("numSubtexturesY")) + settings.numSubtexturesY = js["numSubtexturesY"]; + if (js.contains("particleType")) + settings.particleType = js["particleType"]; + if (js.contains("boxSize")) + settings.boxSize = js["boxSize"]; + if (js.contains("particleDensity")) + settings.particleDensity = js["particleDensity"]; + if (js.contains("particleTexture")) + settings.particleTexture = js["particleTexture"].get(); } catch (const std::exception& e) { logger::error("Precipitation {}: Failed to load from JSON: {}", GetEditorID(), e.what()); } } else { auto& runtime = precipitation->GetRuntimeData(); - + settings.gravityVelocity = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kGravityVelocity).f; settings.rotationVelocity = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kRotationVelocity).f; settings.particleSizeX = precipitation->GetSettingValue(RE::BGSShaderParticleGeometryData::DataID::kParticleSizeX).f; diff --git a/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp b/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp index d7c8ce90f8..ec331e1059 100644 --- a/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp +++ b/src/WeatherEditor/Weather/ReferenceEffectWidget.cpp @@ -16,20 +16,25 @@ void ReferenceEffectWidget::DrawWidget() if (editorWindow->artObjectWidgets.empty()) { ImGui::TextDisabled("No Art Objects available"); } else { - if (WeatherUtils::DrawFormPickerCached("Art Object", settings.artObject, editorWindow->artObjectWidgets, false, true)) changed = true; + if (WeatherUtils::DrawFormPickerCached("Art Object", settings.artObject, editorWindow->artObjectWidgets, false, true)) + changed = true; } ImGui::SeparatorText("Effect Shader"); if (editorWindow->effectShaderWidgets.empty()) { ImGui::TextDisabled("No Effect Shaders available"); } else { - if (WeatherUtils::DrawFormPickerCached("Effect Shader", settings.effectShader, editorWindow->effectShaderWidgets, false, true)) changed = true; + if (WeatherUtils::DrawFormPickerCached("Effect Shader", settings.effectShader, editorWindow->effectShaderWidgets, false, true)) + changed = true; } ImGui::SeparatorText("Flags"); - if (ImGui::Checkbox("Face Target", &settings.faceTarget)) changed = true; - if (ImGui::Checkbox("Attach To Camera", &settings.attachToCamera)) changed = true; - if (ImGui::Checkbox("Inherit Rotation", &settings.inheritRotation)) changed = true; + if (ImGui::Checkbox("Face Target", &settings.faceTarget)) + changed = true; + if (ImGui::Checkbox("Attach To Camera", &settings.attachToCamera)) + changed = true; + if (ImGui::Checkbox("Inherit Rotation", &settings.inheritRotation)) + changed = true; if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); @@ -63,9 +68,12 @@ void ReferenceEffectWidget::LoadSettings() settings.effectShader = nullptr; } } - if (js.contains("faceTarget")) settings.faceTarget = js["faceTarget"]; - if (js.contains("attachToCamera")) settings.attachToCamera = js["attachToCamera"]; - if (js.contains("inheritRotation")) settings.inheritRotation = js["inheritRotation"]; + if (js.contains("faceTarget")) + settings.faceTarget = js["faceTarget"]; + if (js.contains("attachToCamera")) + settings.attachToCamera = js["attachToCamera"]; + if (js.contains("inheritRotation")) + settings.inheritRotation = js["inheritRotation"]; } catch (const std::exception& e) { logger::error("ReferenceEffect {}: Failed to load from JSON: {}", GetEditorID(), e.what()); } @@ -96,7 +104,7 @@ void ReferenceEffectWidget::ApplyChanges() referenceEffect->data.artObject = settings.artObject; referenceEffect->data.effectShader = settings.effectShader; - + referenceEffect->data.flags.reset(); if (settings.faceTarget) referenceEffect->data.flags.set(RE::BGSReferenceEffect::Flag::kFaceTarget); diff --git a/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp b/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp index 09a37765b7..06b5cdc206 100644 --- a/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp +++ b/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp @@ -13,36 +13,48 @@ void VolumetricLightingWidget::DrawWidget() if (ImGui::BeginTabBar("VolumetricLightingTabs")) { if (ImGui::BeginTabItem("Basic")) { ImGui::SeparatorText("Intensity"); - if (WeatherUtils::DrawSliderFloat("Intensity", settings.intensity, 0.0f, 10.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Intensity", settings.intensity, 0.0f, 10.0f)) + changed = true; ImGui::SeparatorText("Custom Color"); - if (WeatherUtils::DrawSliderFloat("Contribution", settings.customColorContribution, 0.0f, 1.0f)) changed = true; - + if (WeatherUtils::DrawSliderFloat("Contribution", settings.customColorContribution, 0.0f, 1.0f)) + changed = true; + ImGui::SeparatorText("RGB Color"); - if (WeatherUtils::DrawSliderFloat("Red", settings.red, 0.0f, 1.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Green", settings.green, 0.0f, 1.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Blue", settings.blue, 0.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Red", settings.red, 0.0f, 1.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Green", settings.green, 0.0f, 1.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Blue", settings.blue, 0.0f, 1.0f)) + changed = true; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Density")) { ImGui::SeparatorText("Density Settings"); - if (WeatherUtils::DrawSliderFloat("Contribution", settings.densityContribution, 0.0f, 1.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Size", settings.densitySize, 0.0f, 10.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Wind Speed", settings.densityWindSpeed, -100.0f, 100.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Falling Speed", settings.densityFallingSpeed, -100.0f, 100.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Contribution", settings.densityContribution, 0.0f, 1.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Size", settings.densitySize, 0.0f, 10.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Wind Speed", settings.densityWindSpeed, -100.0f, 100.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Falling Speed", settings.densityFallingSpeed, -100.0f, 100.0f)) + changed = true; ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Advanced")) { ImGui::SeparatorText("Phase Function"); - if (WeatherUtils::DrawSliderFloat("Contribution", settings.phaseFunctionContribution, 0.0f, 1.0f)) changed = true; - if (WeatherUtils::DrawSliderFloat("Scattering", settings.phaseFunctionScattering, -1.0f, 1.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Contribution", settings.phaseFunctionContribution, 0.0f, 1.0f)) + changed = true; + if (WeatherUtils::DrawSliderFloat("Scattering", settings.phaseFunctionScattering, -1.0f, 1.0f)) + changed = true; ImGui::SeparatorText("Sampling"); - if (WeatherUtils::DrawSliderFloat("Range Factor", settings.samplingRangeFactor, 0.0f, 10.0f)) changed = true; + if (WeatherUtils::DrawSliderFloat("Range Factor", settings.samplingRangeFactor, 0.0f, 10.0f)) + changed = true; ImGui::EndTabItem(); } @@ -64,18 +76,30 @@ void VolumetricLightingWidget::LoadSettings() if (!js.empty()) { try { - if (js.contains("intensity")) settings.intensity = js["intensity"]; - if (js.contains("customColorContribution")) settings.customColorContribution = js["customColorContribution"]; - if (js.contains("red")) settings.red = js["red"]; - if (js.contains("green")) settings.green = js["green"]; - if (js.contains("blue")) settings.blue = js["blue"]; - if (js.contains("densityContribution")) settings.densityContribution = js["densityContribution"]; - if (js.contains("densitySize")) settings.densitySize = js["densitySize"]; - if (js.contains("densityWindSpeed")) settings.densityWindSpeed = js["densityWindSpeed"]; - if (js.contains("densityFallingSpeed")) settings.densityFallingSpeed = js["densityFallingSpeed"]; - if (js.contains("phaseFunctionContribution")) settings.phaseFunctionContribution = js["phaseFunctionContribution"]; - if (js.contains("phaseFunctionScattering")) settings.phaseFunctionScattering = js["phaseFunctionScattering"]; - if (js.contains("samplingRangeFactor")) settings.samplingRangeFactor = js["samplingRangeFactor"]; + if (js.contains("intensity")) + settings.intensity = js["intensity"]; + if (js.contains("customColorContribution")) + settings.customColorContribution = js["customColorContribution"]; + if (js.contains("red")) + settings.red = js["red"]; + if (js.contains("green")) + settings.green = js["green"]; + if (js.contains("blue")) + settings.blue = js["blue"]; + if (js.contains("densityContribution")) + settings.densityContribution = js["densityContribution"]; + if (js.contains("densitySize")) + settings.densitySize = js["densitySize"]; + if (js.contains("densityWindSpeed")) + settings.densityWindSpeed = js["densityWindSpeed"]; + if (js.contains("densityFallingSpeed")) + settings.densityFallingSpeed = js["densityFallingSpeed"]; + if (js.contains("phaseFunctionContribution")) + settings.phaseFunctionContribution = js["phaseFunctionContribution"]; + if (js.contains("phaseFunctionScattering")) + settings.phaseFunctionScattering = js["phaseFunctionScattering"]; + if (js.contains("samplingRangeFactor")) + settings.samplingRangeFactor = js["samplingRangeFactor"]; } catch (const std::exception& e) { logger::error("VolumetricLighting {}: Failed to load from JSON: {}", GetEditorID(), e.what()); } diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index e4b1840700..87a7c188ed 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -71,10 +71,9 @@ void WeatherWidget::DrawWidget() ImGui::SetNextWindowFocus(); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.16f, 0.16f, 0.16f, 1.0f)); - if (ImGui::Begin("##SearchDropdown", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { - + if (ImGui::Begin("##SearchDropdown", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { for (size_t i = 0; i < std::min(size_t(5), searchResults.size()); ++i) { const auto& result = searchResults[i]; std::string label = std::format("{} ({})", result.displayName, result.tabName); @@ -106,113 +105,114 @@ void WeatherWidget::DrawWidget() auto& widgets = editorWindow->weatherWidgets; // Sets the parent widget if settings have been loaded. - if (settings.parent != "None") { - parent = GetParent(); - if (parent == nullptr) - settings.parent = "None"; - } - - if (editorWindow->settings.enableInheritFromParent) { - if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { - // Option for "None" - if (ImGui::Selectable("None", parent == nullptr)) { - parent = nullptr; + if (settings.parent != "None") { + parent = GetParent(); + if (parent == nullptr) settings.parent = "None"; - } + } - for (int i = 0; i < widgets.size(); i++) { - auto& widget = widgets[i]; + if (editorWindow->settings.enableInheritFromParent) { + if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { + // Option for "None" + if (ImGui::Selectable("None", parent == nullptr)) { + parent = nullptr; + settings.parent = "None"; + } - // Skip self-selection - if (widget.get() == this) - continue; + for (int i = 0; i < widgets.size(); i++) { + auto& widget = widgets[i]; - // Option for each widget - if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget.get())) { - parent = (WeatherWidget*)widget.get(); - settings.parent = widget->GetEditorID(); - } + // Skip self-selection + if (widget.get() == this) + continue; - // Set default focus to the current parent - if (parent == widget.get()) { - ImGui::SetItemDefaultFocus(); + // Option for each widget + if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget.get())) { + parent = (WeatherWidget*)widget.get(); + settings.parent = widget->GetEditorID(); + } + + // Set default focus to the current parent + if (parent == widget.get()) { + ImGui::SetItemDefaultFocus(); + } } + ImGui::EndCombo(); } - ImGui::EndCombo(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); - ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "Note: This is NOT the same as cell lighting template inheritance."); - ImGui::EndTooltip(); - } - - if (parent) { ImGui::SameLine(); - if (ImGui::Button("Inherit All")) { - InheritAllFromParent(); - } + ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Copy all parameter values from parent weather"); + ImGui::BeginTooltip(); + ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); + ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "Note: This is NOT the same as cell lighting template inheritance."); + ImGui::EndTooltip(); } - - if (!parent->IsOpen()) { + + if (parent) { ImGui::SameLine(); - if (ImGui::Button("Open")) - parent->SetOpen(true); + if (ImGui::Button("Inherit All")) { + InheritAllFromParent(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Copy all parameter values from parent weather"); + } + + if (!parent->IsOpen()) { + ImGui::SameLine(); + if (ImGui::Button("Open")) + parent->SetOpen(true); + } } } - } -} // Tab bar for organizing settings - if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { - // Use activeTabOverride to auto-navigate to specific tab - ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags dalcFlags = (activeTabOverride == "Lighting (DALC)") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags atmosphereFlags = (activeTabOverride == "Atmosphere Colors") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags recordsFlags = (activeTabOverride == "Records") ? ImGuiTabItemFlags_SetSelected : 0; - if (!activeTabOverride.empty()) { - activeTabOverride = ""; // Clear after use - } + } // Tab bar for organizing settings + if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { + // Use activeTabOverride to auto-navigate to specific tab + ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags dalcFlags = (activeTabOverride == "Lighting (DALC)") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags atmosphereFlags = (activeTabOverride == "Atmosphere Colors") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags recordsFlags = (activeTabOverride == "Records") ? ImGuiTabItemFlags_SetSelected : 0; + if (!activeTabOverride.empty()) { + activeTabOverride = ""; // Clear after use + } - if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { - DrawProperties("Sun", { { "Sun Glare", INT8_SLIDER }, { "Sun Damage", INT8_SLIDER } }); - DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); - DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); - DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, + if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { + DrawProperties("Sun", { { "Sun Glare", INT8_SLIDER }, { "Sun Damage", INT8_SLIDER } }); + DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); + DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); + DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, { "Thunder Lightning Frequency", INT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); - DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); - DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); - ImGui::EndTabItem(); - } if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { - DrawDALCSettings(); - ImGui::EndTabItem(); - } + DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); + DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { + DrawDALCSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Atmosphere Colors", nullptr, atmosphereFlags)) { - DrawWeatherColorSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Atmosphere Colors", nullptr, atmosphereFlags)) { + DrawWeatherColorSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Clouds", nullptr, cloudsFlags)) { - DrawCloudSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Clouds", nullptr, cloudsFlags)) { + DrawCloudSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Fog", nullptr, fogFlags)) { - DrawFogSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Fog", nullptr, fogFlags)) { + DrawFogSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { - DrawFeatureSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { + DrawFeatureSettings(); + ImGui::EndTabItem(); + } if (ImGui::BeginTabItem("Records", nullptr, recordsFlags)) { ImGui::Spacing(); @@ -222,36 +222,37 @@ void WeatherWidget::DrawWidget() ImGui::Spacing(); auto* editorWindow = EditorWindow::GetSingleton(); - bool recordChanged = false; - bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); - WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; - - // ImageSpace Records (per time of day) - if (ImGui::CollapsingHeader("ImageSpace", ImGuiTreeNodeFlags_DefaultOpen)) { - for (int i = 0; i < ColorTimes::kTotal; i++) { - ImGui::PushID(i); - std::string label = ColorTimeLabel(i); - std::string inheritKey = "ImageSpace_" + std::to_string(i); - - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags[inheritKey]; - if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i]; - recordChanged = true; + bool recordChanged = false; + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; + + // ImageSpace Records (per time of day) + if (ImGui::CollapsingHeader("ImageSpace", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "ImageSpace_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i]; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("%s:", label.c_str()); - ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); + if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->imageSpaces[i]) { ImGui::SameLine(); if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { @@ -266,38 +267,39 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this ImageSpace for editing"); } } - + ImGui::PopID(); } ImGui::Spacing(); } - // Volumetric Lighting Records (per time of day) - if (ImGui::CollapsingHeader("Volumetric Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { - for (int i = 0; i < ColorTimes::kTotal; i++) { - ImGui::PushID(100 + i); - std::string label = ColorTimeLabel(i); - std::string inheritKey = "VolumetricLighting_" + std::to_string(i); - - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags[inheritKey]; - if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i]; - recordChanged = true; + // Volumetric Lighting Records (per time of day) + if (ImGui::CollapsingHeader("Volumetric Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(100 + i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "VolumetricLighting_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i]; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("%s:", label.c_str()); - ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); + if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->volumetricLighting[i]) { ImGui::SameLine(); if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { @@ -312,33 +314,34 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Volumetric Lighting for editing"); } } - + ImGui::PopID(); } ImGui::Spacing(); } - // Precipitation Data - if (ImGui::CollapsingHeader("Precipitation", ImGuiTreeNodeFlags_DefaultOpen)) { - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags["Precipitation"]; - if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->precipitationData = parentWidget->weather->precipitationData; - recordChanged = true; + // Precipitation Data + if (ImGui::CollapsingHeader("Precipitation", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["Precipitation"]; + if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->precipitationData = parentWidget->weather->precipitationData; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("Particle Shader:"); - ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("Particle Shader:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); + if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->precipitationData) { ImGui::SameLine(); if (ImGui::SmallButton("Open##Precip")) { @@ -353,31 +356,32 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Precipitation for editing"); } } - + ImGui::Spacing(); } - // Visual Effect (Reference Effect) - if (ImGui::CollapsingHeader("Visual Effect", ImGuiTreeNodeFlags_DefaultOpen)) { - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags["ReferenceEffect"]; - if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->referenceEffect = parentWidget->weather->referenceEffect; - recordChanged = true; + // Visual Effect (Reference Effect) + if (ImGui::CollapsingHeader("Visual Effect", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["ReferenceEffect"]; + if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->referenceEffect = parentWidget->weather->referenceEffect; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("Reference Effect:"); - ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("Reference Effect:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); + if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->referenceEffect) { ImGui::SameLine(); if (ImGui::SmallButton("Open##RefEffect")) { @@ -392,24 +396,24 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Visual Effect for editing"); } } - - - ImGui::Spacing(); - } - if (recordChanged) { - } + ImGui::Spacing(); + } - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); + if (recordChanged) { + } + + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } ImGui::End(); } void WeatherWidget::LoadSettings() { - bool hadErrors = false; if (!js.empty()) { + bool hadErrors = false; + if (!js.empty()) { try { // Attempt to load settings from JSON settings = js; @@ -465,7 +469,7 @@ void WeatherWidget::SaveSettings() try { js = settings; - + if (js.is_null()) { logger::error("Weather {}: Serialization produced null JSON!", GetEditorID()); } else if (!js.contains("weatherProperties")) { @@ -576,7 +580,7 @@ void WeatherWidget::SetWeatherValues() weather->cloudLayerSpeedX[i] = (int8_t)settingsCloud.cloudLayerSpeedX; weather->cloudLayerSpeedY[i] = (int8_t)settingsCloud.cloudLayerSpeedY; - + if (!settingsCloud.enabled) { disabledBits |= (1 << i); } @@ -869,23 +873,23 @@ void WeatherWidget::DrawWeatherColorSettings() // Organized display order: group related sky/fog/lighting properties static const int displayOrder[] = { - 0, // Sky Upper - 7, // Sky Lower - 8, // Horizon - 1, // Fog Near - 12, // Fog Far - 3, // Ambient - 4, // Sunlight - 5, // Sun - 6, // Stars - 9, // Effect Lighting - 10, // Cloud LOD Diffuse - 11, // Cloud LOD Ambient - 13, // Sky Statics - 14, // Water Multiplier - 15, // Sun Glare - 16, // Moon Glare - 2, // Unknown + 0, // Sky Upper + 7, // Sky Lower + 8, // Horizon + 1, // Fog Near + 12, // Fog Far + 3, // Ambient + 4, // Sunlight + 5, // Sun + 6, // Stars + 9, // Effect Lighting + 10, // Cloud LOD Diffuse + 11, // Cloud LOD Ambient + 13, // Sky Statics + 14, // Water Multiplier + 15, // Sun Glare + 16, // Moon Glare + 2, // Unknown }; for (int idx = 0; idx < ColorTypes::kTotal; idx++) { @@ -925,70 +929,73 @@ void WeatherWidget::DrawCloudSettings() bool changed = false; for (int i = 0; i < TESWeather::kTotalLayers; i++) { std::string layer = std::format("Layer {}", i); - + // Default to open if this layer has a texture (is being used) ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (!settings.clouds[i].texturePath.empty()) { flags |= ImGuiTreeNodeFlags_DefaultOpen; } - if (ImGui::CollapsingHeader(layer.c_str(), flags)) { - ImGui::Indent(10.0f); - ImGui::Spacing(); - - bool layerEnabled = settings.clouds[i].enabled; - - // Begin horizontal layout for enable checkbox and sliders on left, texture on right - ImGui::BeginGroup(); - - if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { - settings.clouds[i].enabled = layerEnabled; - changed = true; - } - - ImGui::Spacing(); - ImGui::Spacing(); - - // Make sliders 1/3 width - ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.4f); - if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; - ImGui::Spacing(); - if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; - ImGui::PopItemWidth(); - - ImGui::EndGroup(); - - // Draw texture in upper right if available - if (!settings.clouds[i].texturePath.empty()) { - auto* texture = GetCloudTexture(i); - if (texture) { - ImGui::SameLine(0.0f, 20.0f); - ImGui::BeginGroup(); - float textureSize = 128.0f; - ImGui::Image((void*)texture, ImVec2(textureSize, textureSize)); - // Small grey subtext below image - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); - ImGui::PushFont(ImGui::GetFont()); - ImGui::SetWindowFontScale(0.8f); - float textWidth = ImGui::CalcTextSize(settings.clouds[i].texturePath.c_str()).x; - if (textWidth > textureSize) { - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + textureSize); - ImGui::TextWrapped("%s", settings.clouds[i].texturePath.c_str()); - ImGui::PopTextWrapPos(); - } else { - ImGui::Text("%s", settings.clouds[i].texturePath.c_str()); + if (ImGui::CollapsingHeader(layer.c_str(), flags)) { + ImGui::Indent(10.0f); + ImGui::Spacing(); + + bool layerEnabled = settings.clouds[i].enabled; + + // Begin horizontal layout for enable checkbox and sliders on left, texture on right + ImGui::BeginGroup(); + + if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { + settings.clouds[i].enabled = layerEnabled; + changed = true; + } + + ImGui::Spacing(); + ImGui::Spacing(); + + // Make sliders 1/3 width + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.4f); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) + changed = true; + ImGui::Spacing(); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) + changed = true; + ImGui::PopItemWidth(); + + ImGui::EndGroup(); + + // Draw texture in upper right if available + if (!settings.clouds[i].texturePath.empty()) { + auto* texture = GetCloudTexture(i); + if (texture) { + ImGui::SameLine(0.0f, 20.0f); + ImGui::BeginGroup(); + float textureSize = 128.0f; + ImGui::Image((void*)texture, ImVec2(textureSize, textureSize)); + // Small grey subtext below image + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushFont(ImGui::GetFont()); + ImGui::SetWindowFontScale(0.8f); + float textWidth = ImGui::CalcTextSize(settings.clouds[i].texturePath.c_str()).x; + if (textWidth > textureSize) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + textureSize); + ImGui::TextWrapped("%s", settings.clouds[i].texturePath.c_str()); + ImGui::PopTextWrapPos(); + } else { + ImGui::Text("%s", settings.clouds[i].texturePath.c_str()); + } + ImGui::SetWindowFontScale(1.0f); + ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::EndGroup(); } - ImGui::SetWindowFontScale(1.0f); - ImGui::PopFont(); - ImGui::PopStyleColor(); - ImGui::EndGroup(); } - } - - ImGui::Spacing(); - ImGui::Spacing(); if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { + + ImGui::Spacing(); + ImGui::Spacing(); + if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { TOD::RenderTODHeader(); - + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Separator(); @@ -1025,7 +1032,7 @@ void WeatherWidget::DrawCloudSettings() TOD::EndTODTable(); } - + ImGui::Spacing(); ImGui::Unindent(10.0f); } @@ -1047,7 +1054,7 @@ void WeatherWidget::DrawFogSettings() ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthFixed, 250.0f); ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthFixed, 250.0f); - + // Header row ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -1055,7 +1062,7 @@ void WeatherWidget::DrawFogSettings() ImGui::Text("Day"); ImGui::TableSetColumnIndex(2); ImGui::Text("Night"); - + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Separator(); @@ -1195,7 +1202,7 @@ void WeatherWidget::DrawProperties(std::string category, std::map(ImGui::GetTime()) - highlightStartTime; @@ -1215,7 +1222,7 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.weatherProperties[property]; } else if (settings.weatherColors.find(property) != settings.weatherColors.end()) { @@ -1280,41 +1293,43 @@ void WeatherWidget::InheritFromParent(const std::string& property) void WeatherWidget::InheritAllFromParent() { - if (!HasParent()) return; - + if (!HasParent()) + return; + WeatherWidget* parentWidget = GetParent(); - if (!parentWidget) return; - + if (!parentWidget) + return; + // Copy all weather properties for (const auto& [key, value] : parentWidget->settings.weatherProperties) { settings.weatherProperties[key] = value; } - + // Copy all weather colors for (const auto& [key, value] : parentWidget->settings.weatherColors) { settings.weatherColors[key] = value; } - + // Copy all fog properties for (const auto& [key, value] : parentWidget->settings.fogProperties) { settings.fogProperties[key] = value; } - + // Copy atmosphere colors for (int i = 0; i < ColorTypes::kTotal; i++) { settings.atmosphereColors[i] = parentWidget->settings.atmosphereColors[i]; } - + // Copy DALC settings for (int i = 0; i < ColorTimes::kTotal; i++) { settings.dalc[i] = parentWidget->settings.dalc[i]; } - + // Copy cloud settings for (int i = 0; i < TESWeather::kTotalLayers; i++) { settings.clouds[i] = parentWidget->settings.clouds[i]; } - + // Set all inherit flags to true settings.inheritFlags["DALC_Specular"] = true; settings.inheritFlags["DALC_Fresnel"] = true; @@ -1324,12 +1339,12 @@ void WeatherWidget::InheritAllFromParent() settings.inheritFlags["DALC_DirYMin"] = true; settings.inheritFlags["DALC_DirZMax"] = true; settings.inheritFlags["DALC_DirZMin"] = true; - + settings.inheritFlags["Fog_Near"] = true; settings.inheritFlags["Fog_Far"] = true; settings.inheritFlags["Fog_Power"] = true; settings.inheritFlags["Fog_Max"] = true; - + // Atmosphere colors static const int displayOrder[] = { 0, 7, 8, 1, 12, 3, 4, 5, 6, 9, 10, 11, 13, 14, 15, 16, 2 }; for (int idx = 0; idx < ColorTypes::kTotal; idx++) { @@ -1337,18 +1352,18 @@ void WeatherWidget::InheritAllFromParent() std::string colorTypeLabel = ColorTypeLabel(i); settings.inheritFlags["Atmosphere_" + colorTypeLabel] = true; } - + // Cloud settings for (int i = 0; i < TESWeather::kTotalLayers; i++) { settings.inheritFlags[std::format("Cloud{}_Color", i)] = true; settings.inheritFlags[std::format("Cloud{}_Alpha", i)] = true; } - + // Apply the changes if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } - + EditorWindow::GetSingleton()->ShowNotification( std::format("Inherited all settings from {}", parentWidget->GetEditorID()), ImVec4(0.0f, 1.0f, 0.5f, 1.0f), @@ -1543,35 +1558,33 @@ bool WeatherWidget::ShouldHighlight(const std::string& settingId) const if (highlightedSetting != settingId) return false; -ID3D11ShaderResourceView* WeatherWidget::GetCloudTexture(int layerIndex) -{ - if (cloudTextureCache.contains(layerIndex)) { - return cloudTextureCache[layerIndex]; - } + ID3D11ShaderResourceView* WeatherWidget::GetCloudTexture(int layerIndex) + { + if (cloudTextureCache.contains(layerIndex)) { + return cloudTextureCache[layerIndex]; + } - const auto& texturePath = settings.clouds[layerIndex].texturePath; - if (texturePath.empty()) { - return nullptr; - } + const auto& texturePath = settings.clouds[layerIndex].texturePath; + if (texturePath.empty()) { + return nullptr; + } - // Build resource path for BSA loading: Textures\path (relative to Data folder) - // Note: Skyrim texture paths don't include .dds extension, so we add it - std::string resourcePath = std::string("Textures\\") + texturePath; - - // Add .dds extension if not present - if (resourcePath.size() < 4 || resourcePath.substr(resourcePath.size() - 4) != ".dds") { - resourcePath += ".dds"; - } - - ID3D11ShaderResourceView* srv = nullptr; - ImVec2 textureSize; - - if (Util::LoadDDSTextureFromFile(globals::d3d::device, resourcePath.c_str(), &srv, textureSize)) { - cloudTextureCache[layerIndex] = srv; - return srv; - } - - return nullptr; -} + // Build resource path for BSA loading: Textures\path (relative to Data folder) + // Note: Skyrim texture paths don't include .dds extension, so we add it + std::string resourcePath = std::string("Textures\\") + texturePath; + // Add .dds extension if not present + if (resourcePath.size() < 4 || resourcePath.substr(resourcePath.size() - 4) != ".dds") { + resourcePath += ".dds"; + } + ID3D11ShaderResourceView* srv = nullptr; + ImVec2 textureSize; + + if (Util::LoadDDSTextureFromFile(globals::d3d::device, resourcePath.c_str(), &srv, textureSize)) { + cloudTextureCache[layerIndex] = srv; + return srv; + } + + return nullptr; + } diff --git a/src/WeatherEditor/Weather/WeatherWidget.h b/src/WeatherEditor/Weather/WeatherWidget.h index f04b1fb1c3..cf3f0f5403 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.h +++ b/src/WeatherEditor/Weather/WeatherWidget.h @@ -129,10 +129,10 @@ class WeatherWidget : public Widget void DrawCloudSettings(); void DrawFogSettings(); void DrawFeatureSettings(); - + // Cloud texture loading ID3D11ShaderResourceView* GetCloudTexture(int layerIndex); - + // Search functionality struct SearchResult { diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index 99c6f96264..ff4482a75e 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -1,6 +1,6 @@ #include "WeatherUtils.h" -#include "PaletteWindow.h" #include "EditorWindow.h" +#include "PaletteWindow.h" bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring) { @@ -151,13 +151,13 @@ namespace WeatherUtils static std::map pendingValues; static std::map lastChangeTime; const double debounceDelay = 2.0; - + bool changed = ImGui::SliderInt(label.c_str(), &property, -128, 127); if (changed) { pendingValues[label] = property; lastChangeTime[label] = ImGui::GetTime(); } - + // Check for any pending values that should be tracked std::vector toTrack; for (const auto& [key, changeTime] : lastChangeTime) { @@ -165,14 +165,14 @@ namespace WeatherUtils toTrack.push_back(key); } } - + // Track and remove completed entries for (const auto& key : toTrack) { PaletteWindow::GetSingleton()->TrackValueUsage(key, static_cast(pendingValues[key])); pendingValues.erase(key); lastChangeTime.erase(key); } - + return changed; } @@ -181,11 +181,11 @@ namespace WeatherUtils static std::map colorCache; static std::string activeColorId; static std::map wasPickerOpen; - + std::string cacheId = l; bool isActive = ImGui::IsPopupOpen(l.c_str(), ImGuiPopupFlags_AnyPopupId); bool wasActive = wasPickerOpen[cacheId]; - + // Cache the original color when picker is first activated if (isActive && activeColorId != cacheId) { colorCache[cacheId] = property; @@ -193,7 +193,7 @@ namespace WeatherUtils } else if (!isActive && activeColorId == cacheId) { activeColorId.clear(); } - + // Check for Ctrl+Z while picker is active if (isActive && ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_Z)) { if (colorCache.contains(cacheId)) { @@ -202,23 +202,23 @@ namespace WeatherUtils return true; } } - + bool changed = ImGui::ColorEdit3(l.c_str(), (float*)&property); - + // Track color usage only when picker closes if (wasActive && !isActive) { PaletteWindow::GetSingleton()->TrackColorUsage(property); } - + wasPickerOpen[cacheId] = isActive; - + // Drag-and-drop source if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("COLOR_DND", &property, sizeof(float3)); ImGui::ColorButton("##preview", ImVec4(property.x, property.y, property.z, 1.0f), ImGuiColorEditFlags_NoAlpha); ImGui::EndDragDropSource(); } - + // Drag-and-drop target if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR_DND")) { @@ -230,7 +230,7 @@ namespace WeatherUtils } ImGui::EndDragDropTarget(); } - + return changed; } @@ -244,13 +244,13 @@ namespace WeatherUtils static std::map pendingValues; static std::map lastChangeTime; const double debounceDelay = 2.0; - + bool changed = ImGui::SliderFloat(label.c_str(), &property, min, max); if (changed) { pendingValues[label] = property; lastChangeTime[label] = ImGui::GetTime(); } - + // Check for any pending values that should be tracked std::vector toTrack; for (const auto& [key, changeTime] : lastChangeTime) { @@ -258,14 +258,14 @@ namespace WeatherUtils toTrack.push_back(key); } } - + // Track and remove completed entries for (const auto& key : toTrack) { PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingValues[key]); pendingValues.erase(key); lastChangeTime.erase(key); } - + return changed; } } @@ -385,7 +385,7 @@ namespace TOD static std::map pendingValues; static std::map lastChangeTime; const double debounceDelay = 2.0; - + float factors[4]; GetTimeOfDayFactors(factors); bool changed = false; @@ -406,22 +406,23 @@ namespace TOD if (!isActive) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); - ImGui::PushItemWidth(sliderWidth); - std::string id = std::string("##") + label + std::to_string(i); - if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { - changed = true; - std::string valueName = std::string(label) + " " + GetPeriodName(i); - pendingValues[valueName] = values[i]; - lastChangeTime[valueName] = ImGui::GetTime(); - } + ImGui::PushItemWidth(sliderWidth); + std::string id = std::string("##") + label + std::to_string(i); + if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { + changed = true; + std::string valueName = std::string(label) + " " + GetPeriodName(i); + pendingValues[valueName] = values[i]; + lastChangeTime[valueName] = ImGui::GetTime(); + } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); ImGui::PopItemWidth(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%.0f%%", factors[i] * 100.0f); + ImGui::PopItemWidth(); if (!isActive) ImGui::PopStyleVar(); } - + // Check for any pending values that should be tracked std::vector toTrack; for (const auto& [key, changeTime] : lastChangeTime) { @@ -429,7 +430,7 @@ namespace TOD toTrack.push_back(key); } } - + // Track and remove completed entries for (const auto& key : toTrack) { PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingValues[key]); @@ -448,7 +449,7 @@ namespace TOD ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - + // Only highlight the title text based on active time of day bool anyActive = false; for (int i = 0; i < Count; ++i) { @@ -459,12 +460,12 @@ namespace TOD } if (!anyActive) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); - + ImGui::Text("%s", label); - + if (!anyActive) ImGui::PopStyleVar(); - + ImGui::TableSetColumnIndex(1); float totalWidth = ImGui::GetContentRegionAvail().x; @@ -490,24 +491,24 @@ namespace TOD std::string id = std::string("##") + label + std::to_string(i); ImVec4 color = ImVec4(colors[i].x, colors[i].y, colors[i].z, 1.0f); - + static std::map colorCache; static std::string activeColorId; - + // Use ColorButton with fixed size - no alpha styling on the button itself if (ImGui::ColorButton(id.c_str(), color, ImGuiColorEditFlags_NoAlpha, ImVec2(buttonSize, buttonSize))) { colorCache[id] = colors[i]; activeColorId = id; ImGui::OpenPopup(id.c_str()); } - + // Drag-and-drop source if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("COLOR_DND", &colors[i], sizeof(float3)); ImGui::ColorButton("##preview", color, ImGuiColorEditFlags_NoAlpha); ImGui::EndDragDropSource(); } - + // Drag-and-drop target if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("COLOR_DND")) { @@ -519,12 +520,12 @@ namespace TOD } ImGui::EndDragDropTarget(); } - + // Color picker popup static std::map wasPopupOpen; bool isPopupOpen = ImGui::BeginPopup(id.c_str()); bool wasOpen = wasPopupOpen[id]; - + if (isPopupOpen) { // Check for Ctrl+Z while picker is active if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_Z)) { @@ -533,7 +534,7 @@ namespace TOD changed = true; } } - + if (ImGui::ColorPicker3((id + "_picker").c_str(), (float*)&colors[i], ImGuiColorEditFlags_NoAlpha)) { changed = true; } @@ -541,12 +542,12 @@ namespace TOD } else if (activeColorId == id) { activeColorId.clear(); } - + // Track color usage only when popup closes if (wasOpen && !isPopupOpen) { PaletteWindow::GetSingleton()->TrackColorUsage(colors[i]); } - + wasPopupOpen[id] = isPopupOpen; if (ImGui::IsItemHovered()) @@ -563,27 +564,27 @@ namespace TOD static std::map pendingSliderValues; static std::map sliderLastChangeTime; const double debounceDelay = 2.0; - - float factors[4]; - GetTimeOfDayFactors(factors); - bool changed = false; - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("%s", label); - ImGui::TableSetColumnIndex(1); - - float totalWidth = ImGui::GetContentRegionAvail().x; - float checkboxWidth = 20.0f; - float spacing = ImGui::GetStyle().ItemSpacing.x; - float sliderWidth = (totalWidth - (static_cast(Count) - 1) * spacing - (parentValues ? static_cast(Count) * checkboxWidth : 0)) / static_cast(Count); - - for (int i = 0; i < Count; ++i) { + + float factors[4]; + GetTimeOfDayFactors(factors); + bool changed = false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + + float totalWidth = ImGui::GetContentRegionAvail().x; + float checkboxWidth = 20.0f; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float sliderWidth = (totalWidth - (static_cast(Count) - 1) * spacing - (parentValues ? static_cast(Count) * checkboxWidth : 0)) / static_cast(Count); + + for (int i = 0; i < Count; ++i) { if (i > 0) ImGui::SameLine(); ImGui::BeginGroup(); - + // Per-column inherit checkbox if (parentValues) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1)); @@ -629,10 +630,10 @@ namespace TOD if (!isActive || (inheritFlags && inheritFlags[i])) ImGui::PopStyleVar(); - + ImGui::EndGroup(); } - + // Check for any pending values that should be tracked std::vector toTrack; for (const auto& [key, changeTime] : sliderLastChangeTime) { @@ -640,7 +641,7 @@ namespace TOD toTrack.push_back(key); } } - + // Track and remove completed entries for (const auto& key : toTrack) { PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingSliderValues[key]); @@ -659,7 +660,7 @@ namespace TOD ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - + bool anyActive = false; for (int i = 0; i < Count; ++i) { if (factors[i] > 0.0f) { @@ -672,16 +673,16 @@ namespace TOD // Draw label text ImGui::Text("%s", label); - + // Draw inherit checkbox right under the label if (parentColors) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); - - std::string inheritId = std::string("##inherit_") + label; - if (ImGui::Checkbox(inheritId.c_str(), &inheritFlag)) { + + std::string inheritId = std::string("##inherit_") + label; + if (ImGui::Checkbox(inheritId.c_str(), &inheritFlag)) { if (inheritFlag) { // Copy all parent values for (int i = 0; i < Count; ++i) { @@ -691,15 +692,15 @@ namespace TOD } // Allow unchecking } - + ImGui::PopStyleVar(); ImGui::PopStyleColor(2); - + if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Inherit from parent weather"); } } - + if (!anyActive) ImGui::PopStyleVar(); @@ -806,7 +807,8 @@ namespace TOD float columnWidth = (totalWidth - 3 * spacing) / 4.0f; for (int i = 0; i < Count; ++i) { - if (i > 0) ImGui::SameLine(); + if (i > 0) + ImGui::SameLine(); ImGui::PushID(i); ImGui::SetNextItemWidth(columnWidth); if (ImGui::SliderFloat("##value", &values[i], minValue, maxValue, format)) { @@ -826,16 +828,16 @@ namespace TOD ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); - + ImGui::Text("%s", label); - + // Draw inherit checkbox if (parentValues) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.6f, 0.6f, 0.6f, 1.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); - + std::string inheritId = std::string("##inherit_") + label; if (ImGui::Checkbox(inheritId.c_str(), &inheritFlag)) { if (inheritFlag) { @@ -845,10 +847,10 @@ namespace TOD changed = true; } } - + ImGui::PopStyleVar(); ImGui::PopStyleColor(2); - + if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Inherit from parent weather"); } @@ -862,13 +864,14 @@ namespace TOD ImGui::BeginDisabled(inheritFlag); for (int i = 0; i < Count; ++i) { - if (i > 0) ImGui::SameLine(); - + if (i > 0) + ImGui::SameLine(); + // Apply inherited value if flag is set if (inheritFlag && parentValues) { values[i] = parentValues[i]; } - + ImGui::PushID(i); ImGui::SetNextItemWidth(columnWidth); if (ImGui::SliderFloat("##value", &values[i], minValue, maxValue, format)) { diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h index 138805ac88..e057ab8d43 100644 --- a/src/WeatherEditor/WeatherUtils.h +++ b/src/WeatherEditor/WeatherUtils.h @@ -95,7 +95,7 @@ namespace WeatherUtils bool DrawFormPickerCached(const char* label, T*& currentForm, const WidgetContainer& widgets, bool showFormID = true, bool allowNone = true, float width = 450.0f) { bool changed = false; - + std::string previewText; if (currentForm) { // Find the widget for current form @@ -109,7 +109,7 @@ namespace WeatherUtils if (editorID.empty()) { editorID = std::format("{:08X}", currentForm->GetFormID()); } - + if (showFormID) { previewText = std::format("{} (0x{:08X})", editorID, currentForm->GetFormID()); } else { @@ -118,17 +118,17 @@ namespace WeatherUtils } else { previewText = "None"; } - + if (width > 0.0f) { ImGui::SetNextItemWidth(width); } - + if (ImGui::BeginCombo(label, previewText.c_str())) { if (allowNone && ImGui::Selectable("None", currentForm == nullptr)) { currentForm = nullptr; changed = true; } - + for (const auto& widget : widgets) { if (widget && widget->form) { T* form = static_cast(widget->form); @@ -139,7 +139,7 @@ namespace WeatherUtils } else { comboLabel = editorID; } - + bool isSelected = (currentForm == form); if (ImGui::Selectable(comboLabel.c_str(), isSelected)) { currentForm = form; @@ -152,26 +152,27 @@ namespace WeatherUtils } ImGui::EndCombo(); } - + return changed; } - + // Legacy form picker (slow - only use if widgets not available) template bool DrawFormPicker(const char* label, T*& currentForm, const Container& formArray, bool showFormID = true, bool allowNone = true, float width = 450.0f) { bool changed = false; - + auto GetFormEditorIDSafe = [](T* form) -> std::string { - if (!form) return ""; - + if (!form) + return ""; + const char* editorID = form->GetFormEditorID(); if (editorID && editorID[0] != '\0') return std::string(editorID); - + return std::format("{:08X}", form->GetFormID()); }; - + std::string previewText; if (currentForm) { std::string editorID = GetFormEditorIDSafe(currentForm); @@ -183,17 +184,17 @@ namespace WeatherUtils } else { previewText = "None"; } - + if (width > 0.0f) { ImGui::SetNextItemWidth(width); } - + if (ImGui::BeginCombo(label, previewText.c_str())) { if (allowNone && ImGui::Selectable("None", currentForm == nullptr)) { currentForm = nullptr; changed = true; } - + for (auto form : formArray) { if (form) { std::string editorID = GetFormEditorIDSafe(form); @@ -203,7 +204,7 @@ namespace WeatherUtils } else { comboLabel = editorID; } - + bool isSelected = (currentForm == form); if (ImGui::Selectable(comboLabel.c_str(), isSelected)) { currentForm = form; @@ -216,7 +217,7 @@ namespace WeatherUtils } ImGui::EndCombo(); } - + return changed; } } \ No newline at end of file diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index 5169ad7382..f5182247ef 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -47,7 +47,7 @@ void Widget::Save() settingsFile.close(); return; } - + settingsFile << js.dump(2); settingsFile.flush(); @@ -58,7 +58,7 @@ void Widget::Save() } settingsFile.close(); - + } catch (const nlohmann::json::exception& e) { logger::error("{}: JSON error while saving settings: {}", GetEditorID(), e.what()); settingsFile.close(); @@ -110,7 +110,7 @@ void Widget::Load() LoadSettings(); return; } - + LoadSettings(); EditorWindow::GetSingleton()->ShowNotification( @@ -152,7 +152,7 @@ void Widget::Delete() try { std::filesystem::remove(filePath); - + js = json(); // Reload settings from vanilla/mod defaults diff --git a/src/WeatherEditor/Widget.h b/src/WeatherEditor/Widget.h index a072db5823..1e166d3ffc 100644 --- a/src/WeatherEditor/Widget.h +++ b/src/WeatherEditor/Widget.h @@ -43,13 +43,15 @@ class Widget virtual std::string GetFormID() const { - if (!form) return "00000000"; + if (!form) + return "00000000"; return std::format("{:08X}", form->GetFormID()); } virtual std::string GetFilename() const { - if (!form) return "Invalid"; + if (!form) + return "Invalid"; if (auto file = form->GetFile()) return std::format("{}", file->GetFilename()); return "Generated"; From a823ae1017bda8a0fb7f0b8cc20d2e1f8a0baf60 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 10 Dec 2025 12:06:35 +1000 Subject: [PATCH 10/30] Undo function, extra safeguards, fixes --- .../Icons/Action Icons/undo.png | Bin 0 -> 2352 bytes src/Menu.cpp | 1 + src/Menu.h | 3 +- src/Utils/UI.cpp | 1 + src/WeatherEditor/EditorWindow.cpp | 179 +++++++++++++----- src/WeatherEditor/EditorWindow.h | 14 ++ src/WeatherEditor/Weather/WeatherWidget.cpp | 81 +++++++- src/WeatherEditor/Widget.h | 3 +- 8 files changed, 229 insertions(+), 53 deletions(-) create mode 100644 package/Interface/CommunityShaders/Icons/Action Icons/undo.png diff --git a/package/Interface/CommunityShaders/Icons/Action Icons/undo.png b/package/Interface/CommunityShaders/Icons/Action Icons/undo.png new file mode 100644 index 0000000000000000000000000000000000000000..78043ca422507b09b6cef935f04e8bac8a04a00a GIT binary patch literal 2352 zcmV-03D5S4P)@~0drDELIAGL9O(c600d`2O+f$vv5yPFU{e85ft?CsDv+oEIv94mD=ds1OWxVl zu078=iy-9KmR`@ye$D_RLPV8qQ`>4<^I+R&+x~-6CRV%)+vc>N+BO5F9K5(cXc%hE zCBW85f4bwo@E76&O5PkOZp$1>zF2|oD4Yt)AkL8hE6fLbd5>^jLCFhpad&8^Y8UFJ z2XcmpSV5lJ%Y$tOay&abJNpTx7qMOREo8-IUPI|kG=Z)4PVD6h@>EcI6U9Z7n|-Pg zho9C(KZDW>aSQh1#_S9|)!|ZqVz}rSad#7q!#xod*vH*Bh))Hj7h*BkyUmp2R2?q$ zK*WN5+?^sVy28{05ev2g^JGTq)t-g5;LFZu;7JwZKgSc>+J6JP`vgtr%HH6+|9f*R z-r*kv;^XcZOL@GmeCywv+fxW)Uz-J9`gYG(D|&;%2(n=N5V%A28K*;I+rqZ5a2_Ok zD?eD^&Un}0bXnLov+X`t>kbi^XQ~X8mW6FIEA%I1#6bdEaql3rzI%)`wz0^w9ISe< zz<%-m7D^vxwtXB^=s^PexXVK#(H`pwFvCbf_ZB$P;{BdL#sz!bb`hEQ^(jg^HZ)-c+`UJ8}l?|iuGN^ zS@Wf|%id|h2^a4bhp}x=!e_9yTCm35(Ydykd`QX zaNC3`q$Nn?ZJn%==b5AIndY9YZ8N_`=(%ki9~-3M#&>uRVw=DX$|mX&Q!h4mf>>w{ z2MywL<7X2GvrFKTH=I>SgtzKcLf;)8aR=AeSx>mWchE%HOi&JNcb4dZhL|_7~5_j0dI??u*^7Pk;OQk z-1C!4U$KeInliZ@8&fJz=n5G?hvx>{2Y+$QzW3j|qrGOkVQrwAMSYk z+&~uYAMHI4cIdWQgYjl|`oc#|ULq67O@DIdOM0H0mlzkj{xkUSnHQV)NV9btz3Krs!E?e_)GuXlOw!(XPe?X79QoJUI%g3j zjS{4Khdg8RQp| z$i*WTyCzvu=L|Url8wiC@5CXO?te<*V%Jnqtp}2I)fGy>e$dx+*pREH!U}ni8+Ftq zLs{FcXzjx1J#KEb8xaNK?@@~1&medvD?h6{~zjRfT7H*CT$!vX^p8FRwaGSxZp6|c4Z<;PCyUok4TfRhMAp2O zarlmK%()fl3Wx7)Hqf-fu@LFdvIU3EcvMhg&>$RhMsAx&sGHRqM#7L#Dl4{Ni5U6> zN|xwM){!G6u$Ft3M`U!u0yhXpU9>zB0yk0?Y@kS!l?Cf$hdeJ3P9r7o*vUJO5rc46 zd$J_PuPU%S2H~tM_#478Iwvhog>Y5^ODrz4xN5~hklgvArBn=NvdC(4BuG8WZ|8mv zxj{G;6e%i%v+QnsUm%=H<8U~%PEi%gD)hDj-$Mpn8xE&}3=95abvB9Q>NI4Pe5^!B z?u6lUPWH00U>}DU2}G%c#BZCGqJlCA za{JMxFuUGlPBLgg%Yg_!`-kr9ZY~!}9C~8-+xP}bFDPth>n3-2t|6#$xC(I4B!_|igq)rcPo^-#4C;9KC7_oEjvb zH@bi7Mv}K;K6a|{*hBJhmoY;=gR~VhMI;AJfqfP;l4ba_CD|Tz%LFG5KLe8G@DNC{ zSx{RhI8jJmTqbO!LN0zvGr@^r@A;182|k=AJxAJQN+;0qA{XkUV{VIq1mx26#-OGn z9zu~Djl}SPjO-**#1C525oYco=lNn4rVMw@TRyOKt)ESbP3GN&oJ)=oZ(C}L<@G3D zJnnw@Vy)RfpPJ$lca_e*!3T=Zl*%fqF(k)W2x%5?C|;*)lPP5^G_7kZr{ESTp?F>Y zZA>YnRVjKr-D8yd#~4srk(ruYT?2!}kh9harLTnkfB+=w2hZRtp`@m*w(|9{0?CI$ zu1C)Y>Nt;Bg>eqo@jYIKq^J@p$}ytIT^4W;q=7pU%N0=fo}BbTwqpM4uWcKXnQhmh z`Nzbnu8`~DO^_$ej-R*U=Ej{B_8J0+met8ziYSG#Xq|uBKc1`QIph4S2YdouG&POdoH z3gG18G_U03e2|<)P4l>bUw^iON>n6EMsWgf!;0Rn<(f`w%i{vOt-k48x&7??eOa2A+xP`@ WZe8ic#%==u0000A> literal 0 HcmV?d00001 diff --git a/src/Menu.cpp b/src/Menu.cpp index 7eaaa78d13..e1f3e2b1ef 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -182,6 +182,7 @@ Menu::~Menu() uiIcons.featureSettingRevert.Release(); uiIcons.applyToGame.Release(); uiIcons.pauseTime.Release(); + uiIcons.undo.Release(); uiIcons.discord.Release(); uiIcons.characters.Release(); uiIcons.display.Release(); diff --git a/src/Menu.h b/src/Menu.h index 7c52d72ee0..37652f9ec2 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -193,8 +193,9 @@ class Menu UIIcon featureSettingRevert; // Feature revert settings icon UIIcon applyToGame; // Apply changes to game icon (weather editor) UIIcon pauseTime; // Pause time icon (weather editor) + UIIcon undo; // Undo icon (weather editor) - // Social media/external link icons + // Social media/external link icons UIIcon discord; // Category icons diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 971bd2f98b..f56f4fae4e 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -318,6 +318,7 @@ namespace Util loadIconWithLogging(basePath + "Action Icons\\restore-settings.png", &menu->uiIcons.featureSettingRevert.texture, menu->uiIcons.featureSettingRevert.size, "restore-settings"); loadIconWithLogging(basePath + "Action Icons\\apply-to-game.png", &menu->uiIcons.applyToGame.texture, menu->uiIcons.applyToGame.size, "apply-to-game"); loadIconWithLogging(basePath + "Action Icons\\pause.png", &menu->uiIcons.pauseTime.texture, menu->uiIcons.pauseTime.size, "pause"); + loadIconWithLogging(basePath + "Action Icons\\undo.png", &menu->uiIcons.undo.texture, menu->uiIcons.undo.size, "undo"); loadIconWithLogging(basePath + "Action Icons\\discord.png", &menu->uiIcons.discord.texture, menu->uiIcons.discord.size, "discord"); // Load category icons in a more compact way diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 83e12cc8bd..a04dad8f26 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -396,33 +396,22 @@ void EditorWindow::ShowObjectsWindow() } } - // Get current cell's lighting template for prioritization - RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; - if (selectedCategory == "Lighting Template") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto& cellData = player->parentCell->GetRuntimeData(); - currentCellLightingTemplate = cellData.lightingTemplate; - } - - // Get current cell's lighting template for prioritization - RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; - if (selectedCategory == "Lighting Template") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto& cellData = player->parentCell->GetRuntimeData(); - currentCellLightingTemplate = cellData.lightingTemplate; - } - } - - // Filtered display of widgets - show current cell's lighting template first - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - for (int i = 0; i < sortedWidgets.size(); ++i) { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) - continue; + // Get current cell's lighting template for prioritization + RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; + if (selectedCategory == "Lighting Template") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto& cellData = player->parentCell->GetRuntimeData(); + currentCellLightingTemplate = cellData.lightingTemplate; + } + } - if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + // Filtered display of widgets - show current cell's lighting template first + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + for (int i = 0; i < sortedWidgets.size(); ++i) { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) + continue; if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) continue; // Apply quick filters @@ -451,18 +440,16 @@ void EditorWindow::ShowObjectsWindow() // Editor ID column with [CURRENT] prefix bool isSelected = sortedWidgets[i]->IsOpen(); if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); - } - } - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID()); - } - - // Context menu + if (ImGui::IsMouseDoubleClicked(0)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } + } + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } // Context menu if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; @@ -889,6 +876,13 @@ void EditorWindow::RenderUI() open = false; } + // Check for Ctrl+Z to undo + if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_Z, false)) { + if (CanUndo()) { + PerformUndo(); + } + } + if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Save All Open Widgets", "Ctrl+S")) { @@ -1088,6 +1082,7 @@ void EditorWindow::RenderUI() ImGui::BulletText("Use quick filters for fast sorting"); ImGui::BulletText("Auto-Apply updates game live"); ImGui::BulletText("Lock weather to prevent changes"); + ImGui::BulletText("Undo button reverts recent changes (Ctrl+Z)"); ImGui::Separator(); ImGui::Text("Total Objects:"); ImGui::BulletText("Weathers: %d", (int)weatherWidgets.size()); @@ -1132,15 +1127,48 @@ void EditorWindow::RenderUI() } } - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); - } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); + } + } + + // Undo button + if (menu && menu->uiIcons.undo.texture) { + bool canUndo = CanUndo(); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + if (!canUndo) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.25f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.5f)); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); } - // Weather lock indicator + const float menuBarHeight = ImGui::GetFrameHeight(); + const float buttonDim = menuBarHeight * 0.85f; + const ImVec2 buttonSize(buttonDim, buttonDim); + + if (ImGui::ImageButton("##GlobalUndo", menu->uiIcons.undo.texture, buttonSize) && canUndo) { + PerformUndo(); + } + + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(); + + if (ImGui::IsItemHovered()) { + if (canUndo) { + ImGui::SetTooltip("Undo (Ctrl+Z) - %d states", (int)undoStack.size()); + } else { + ImGui::SetTooltip("Undo (Ctrl+Z) - No changes to undo"); + } + } + } // Weather lock indicator if (weatherLockActive && lockedWeather) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); @@ -1712,7 +1740,7 @@ void EditorWindow::DisableVanityCamera() } } -void EditorWindow::RestoreVanityCamera() + void EditorWindow::RestoreVanityCamera() { if (!vanityCameraDisabled) return; @@ -1725,6 +1753,67 @@ void EditorWindow::RestoreVanityCamera() } } +void EditorWindow::PushUndoState(Widget* widget) +{ + if (!widget) + return; + + UndoState state; + state.widget = widget; + state.widgetId = widget->GetEditorID(); + state.settings = widget->js; + + undoStack.push_back(state); + + if (undoStack.size() > maxUndoStates) { + undoStack.erase(undoStack.begin()); + } +} + +void EditorWindow::PerformUndo() +{ + if (undoStack.empty()) + return; + + UndoState state = undoStack.back(); + undoStack.pop_back(); + + if (!state.widget) { + for (auto& w : weatherWidgets) { + if (w->GetEditorID() == state.widgetId) { + state.widget = w.get(); + break; + } + } + if (!state.widget) { + for (auto& w : imageSpaceWidgets) { + if (w->GetEditorID() == state.widgetId) { + state.widget = w.get(); + break; + } + } + } + if (!state.widget) { + for (auto& w : lightingTemplateWidgets) { + if (w->GetEditorID() == state.widgetId) { + state.widget = w.get(); + break; + } + } + } + } + + if (state.widget) { + state.widget->js = state.settings; + state.widget->LoadSettings(); + state.widget->ApplyChanges(); + ShowNotification( + std::format("Undone changes to {}", state.widgetId), + ImVec4(0.3f, 0.8f, 1.0f, 1.0f), + 2.0f); + } +} + void EditorWindow::ShowNotification(const std::string& message, const ImVec4& color, float duration) { Notification notif; diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index 38040fe32c..91c241883c 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -80,6 +80,20 @@ class EditorWindow void DisableVanityCamera(); void RestoreVanityCamera(); + // Undo system + struct UndoState + { + Widget* widget; + json settings; + std::string widgetId; + }; + std::vector undoStack; + static const size_t maxUndoStates = 50; + + void PushUndoState(Widget* widget); + void PerformUndo(); + bool CanUndo() const { return !undoStack.empty(); } + // Notification system struct Notification { diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index e4b1840700..e2373ca575 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -3,6 +3,7 @@ #include #include "../EditorWindow.h" +#include "FeatureIssues.h" #include "State.h" #include "Utils/UI.h" #include "WeatherManager.h" @@ -181,7 +182,7 @@ void WeatherWidget::DrawWidget() } if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { - DrawProperties("Sun", { { "Sun Glare", INT8_SLIDER }, { "Sun Damage", INT8_SLIDER } }); + DrawProperties("Sun", { { "Sun Damage", INT8_SLIDER } }); DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, @@ -876,15 +877,15 @@ void WeatherWidget::DrawWeatherColorSettings() 12, // Fog Far 3, // Ambient 4, // Sunlight + 15, // Sun Glare 5, // Sun 6, // Stars + 16, // Moon Glare 9, // Effect Lighting 10, // Cloud LOD Diffuse 11, // Cloud LOD Ambient 13, // Sky Statics 14, // Water Multiplier - 15, // Sun Glare - 16, // Moon Glare 2, // Unknown }; @@ -926,9 +927,8 @@ void WeatherWidget::DrawCloudSettings() for (int i = 0; i < TESWeather::kTotalLayers; i++) { std::string layer = std::format("Layer {}", i); - // Default to open if this layer has a texture (is being used) ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; - if (!settings.clouds[i].texturePath.empty()) { + if (settings.clouds[i].enabled && !settings.clouds[i].texturePath.empty()) { flags |= ImGuiTreeNodeFlags_DefaultOpen; } @@ -943,6 +943,10 @@ void WeatherWidget::DrawCloudSettings() if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { settings.clouds[i].enabled = layerEnabled; + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + EditorWindow::GetSingleton()->PushUndoState(this); + ApplyChanges(); + } changed = true; } @@ -1031,6 +1035,7 @@ void WeatherWidget::DrawCloudSettings() } } if (changed && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + EditorWindow::GetSingleton()->PushUndoState(this); ApplyChanges(); } } @@ -1371,6 +1376,66 @@ void WeatherWidget::LoadFeatureSettings() auto* weatherManager = WeatherManager::GetSingleton(); auto* globalRegistry = WeatherVariables::GlobalWeatherRegistry::GetSingleton(); + // First, validate that all feature settings in the JSON exist as loaded features. + // Prevents loading a .json that references features that aren't installed. (Will load only settings for installed features.) + // Should make it very obvious to users when they have not followed the correct installation instructions. + if (js.contains("featureSettings") && js["featureSettings"].is_object()) { + std::vector missingFeatures; + + for (const auto& [featureName, featureJson] : js["featureSettings"].items()) { + if (featureJson.empty()) { + continue; + } + + // Check if this feature exists and is loaded + bool featureExists = false; + for (auto* feature : Feature::GetFeatureList()) { + if (feature && feature->loaded && feature->GetShortName() == featureName) { + featureExists = true; + break; + } + } + + if (!featureExists) { + missingFeatures.push_back(featureName); + } + } + + // If we found missing features, warn the user + if (!missingFeatures.empty()) { + std::string missingList; + for (size_t i = 0; i < missingFeatures.size(); ++i) { + if (i > 0) missingList += ", "; + missingList += missingFeatures[i]; + } + + // Show notification + EditorWindow::GetSingleton()->ShowNotification( + std::format("Warning: {} references missing feature(s): {}", GetEditorID(), missingList), + ImVec4(1.0f, 0.6f, 0.0f, 1.0f), + 5.0f); + + // Add to Feature Issues system for each missing feature + for (const auto& featureName : missingFeatures) { + FeatureIssues::FeatureFileInfo fileInfo; + fileInfo.featureName = featureName; + + FeatureIssues::AddFeatureIssue( + featureName, + "", + std::format("Weather '{}' contains settings for this feature, but the feature is not loaded. " + "The weather-specific parameters will be ignored until the feature is installed and loaded.", + GetEditorID()), + FeatureIssues::FeatureIssueInfo::IssueType::UNKNOWN, + fileInfo, + ""); + } + + logger::warn("{}: JSON contains feature settings for features that are not loaded: {}", GetEditorID(), missingList); + } + } + + // Now load settings for features that ARE loaded for (auto* feature : Feature::GetFeatureList()) { if (!feature || !feature->loaded) { continue; @@ -1472,7 +1537,7 @@ void WeatherWidget::UpdateSearchResults() // Search in Basic tab properties std::vector>> basicCategories = { - { "Sun", { { "Sun Glare", 0 }, { "Sun Damage", 0 } } }, + { "Sun", { { "Sun Damage", 0 } } }, { "Wind", { { "Wind Speed", 0 }, { "Wind Direction", 0 }, { "Wind Direction Range", 0 } } }, { "Precipitation", { { "Precipitation Begin Fade In", 0 }, { "Precipitation Begin Fade Out", 0 } } }, { "Lightning", { { "Thunder Lightning Begin Fade In", 0 }, { "Thunder Lightning End Fade Out", 0 }, @@ -1543,6 +1608,10 @@ bool WeatherWidget::ShouldHighlight(const std::string& settingId) const if (highlightedSetting != settingId) return false; + float elapsed = static_cast(ImGui::GetTime()) - highlightStartTime; + return elapsed < 2.0f; +} + ID3D11ShaderResourceView* WeatherWidget::GetCloudTexture(int layerIndex) { if (cloudTextureCache.contains(layerIndex)) { diff --git a/src/WeatherEditor/Widget.h b/src/WeatherEditor/Widget.h index a072db5823..38b8680cef 100644 --- a/src/WeatherEditor/Widget.h +++ b/src/WeatherEditor/Widget.h @@ -139,8 +139,9 @@ class Widget bool MatchesSearch(const std::string& text) const; -protected: json js = json(); + +protected: std::string cachedEditorID; virtual void DrawMenu(); std::string GetFolderName(); From cd86446d4eb0e104adfbebeed6498dc009e40346 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:08:10 +0000 Subject: [PATCH 11/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Menu.h | 2 +- src/WeatherEditor/EditorWindow.cpp | 469 ++++++++------- src/WeatherEditor/Weather/WeatherWidget.cpp | 634 ++++++++++---------- 3 files changed, 562 insertions(+), 543 deletions(-) diff --git a/src/Menu.h b/src/Menu.h index 37652f9ec2..c75a341d4f 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -195,7 +195,7 @@ class Menu UIIcon pauseTime; // Pause time icon (weather editor) UIIcon undo; // Undo icon (weather editor) - // Social media/external link icons + // Social media/external link icons UIIcon discord; // Category icons diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index a04dad8f26..89dc1a0f99 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1,10 +1,10 @@ #include "EditorWindow.h" #include "Features/WeatherEditor.h" +#include "PaletteWindow.h" #include "State.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" -#include "PaletteWindow.h" #include "imgui_internal.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, enableInheritFromParent, editorUIScale, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) @@ -49,7 +49,7 @@ void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool /*filled*/) float r = (i % 2 == 0) ? radius : radius * 0.38f; points[i] = ImVec2(center.x + cosf(angle) * r, center.y + sinf(angle) * r); } - + for (int i = 0; i < 10; i++) { drawList->AddLine(points[i], points[(i + 1) % 10], color, 1.5f); } @@ -154,49 +154,50 @@ void EditorWindow::ShowObjectsWindow() ImGui::Text("Categories"); ImGui::Spacing(); - // List of categories - const char* categories[] = { "Lighting Template", "Weather", "WorldSpace", "Cell Lighting", "ImageSpace", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect" }; - for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { - // Highlight the selected category - if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { - selectedCategory = categories[i]; // Update selected category - } - } // Right column: Objects - ImGui::TableSetColumnIndex(1); - - // Display current active weather - auto sky = globals::game::sky; - if (sky && sky->currentWeather) { - auto currentWeather = sky->currentWeather; - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); - ImGui::Text("Current Active Weather:"); - ImGui::PopStyleColor(); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", currentWeather->GetFormEditorID()); - ImGui::SameLine(); - ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); - - // Add button to open the current weather - ImGui::SameLine(); - if (ImGui::SmallButton("Open##CurrentWeather")) { - for (auto& widget : weatherWidgets) { - if (widget->form == currentWeather) { - widget->SetOpen(true); - break; + // List of categories + const char* categories[] = { "Lighting Template", "Weather", "WorldSpace", "Cell Lighting", "ImageSpace", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect" }; + for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { + // Highlight the selected category + if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { + selectedCategory = categories[i]; // Update selected category + } + } // Right column: Objects + ImGui::TableSetColumnIndex(1); + + // Display current active weather + auto sky = globals::game::sky; + if (sky && sky->currentWeather) { + auto currentWeather = sky->currentWeather; + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); + ImGui::Text("Current Active Weather:"); + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", currentWeather->GetFormEditorID()); + ImGui::SameLine(); + ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); + + // Add button to open the current weather + ImGui::SameLine(); + if (ImGui::SmallButton("Open##CurrentWeather")) { + for (auto& widget : weatherWidgets) { + if (widget->form == currentWeather) { + widget->SetOpen(true); + break; + } } } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - } - // Handle Ctrl+F to focus search bar - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { - if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { - ImGui::SetKeyboardFocusHere(); + // Handle Ctrl+F to focus search bar + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(); + } } - } ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); + ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); ImGui::SameLine(); HelpMarker("Type a part of an object name to filter the list.\nCtrl+F: Focus search\nEnter: Open selected"); @@ -227,7 +228,8 @@ void EditorWindow::ShowObjectsWindow() ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Recent:"); ImGui::SameLine(); for (size_t i = 0; i < std::min(size_t(5), recentIt->second.size()); ++i) { - if (i > 0) ImGui::SameLine(); + if (i > 0) + ImGui::SameLine(); if (ImGui::SmallButton(recentIt->second[i].c_str())) { // Find and open widget in current category's collection auto& widgets = selectedCategory == "Weather" ? weatherWidgets : @@ -239,7 +241,7 @@ void EditorWindow::ShowObjectsWindow() selectedCategory == "Lens Flare" ? lensFlareWidgets : selectedCategory == "Visual Effect" ? referenceEffectWidgets : weatherWidgets; - + for (auto& widget : widgets) { if (widget->GetEditorID() == recentIt->second[i]) { widget->SetOpen(true); @@ -274,27 +276,27 @@ void EditorWindow::ShowObjectsWindow() } } - // Display objects based on the selected category - std::vector> emptyWidgets; - const auto& widgets = selectedCategory == "Weather" ? weatherWidgets : - selectedCategory == "WorldSpace" ? worldSpaceWidgets : - selectedCategory == "Cell Lighting" ? emptyWidgets : - selectedCategory == "ImageSpace" ? imageSpaceWidgets : - selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : - selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : - selectedCategory == "Lens Flare" ? lensFlareWidgets : - selectedCategory == "Visual Effect" ? referenceEffectWidgets : - lightingTemplateWidgets; - // Sort widgets based on current sort column - std::vector sortedWidgets; - sortedWidgets.reserve(widgets.size()); - for (const auto& w : widgets) { - sortedWidgets.push_back(w.get()); - } - if (currentSortColumn != SortColumn::None) { - std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { - int comparison = 0; - switch (currentSortColumn) { + // Display objects based on the selected category + std::vector> emptyWidgets; + const auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + selectedCategory == "Cell Lighting" ? emptyWidgets : + selectedCategory == "ImageSpace" ? imageSpaceWidgets : + selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : + selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : + selectedCategory == "Lens Flare" ? lensFlareWidgets : + selectedCategory == "Visual Effect" ? referenceEffectWidgets : + lightingTemplateWidgets; + // Sort widgets based on current sort column + std::vector sortedWidgets; + sortedWidgets.reserve(widgets.size()); + for (const auto& w : widgets) { + sortedWidgets.push_back(w.get()); + } + if (currentSortColumn != SortColumn::None) { + std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { + int comparison = 0; + switch (currentSortColumn) { case SortColumn::EditorID: comparison = _stricmp(a->GetEditorID().c_str(), b->GetEditorID().c_str()); break; @@ -315,103 +317,104 @@ void EditorWindow::ShowObjectsWindow() } default: break; - } - return sortAscending ? (comparison < 0) : (comparison > 0); - }); - } + } + return sortAscending ? (comparison < 0) : (comparison > 0); + }); + } - // Special handling for Cell Lighting category - if (selectedCategory == "Cell Lighting") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto cell = player->parentCell; - bool isInterior = cell->IsInteriorCell(); - - if (isInterior) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - - // No favorite star for cell lighting (it's always the current cell) - ImGui::Dummy(ImVec2(24, 24)); - - ImGui::TableNextColumn(); - - // Display current cell name - const char* cellName = cell->GetName(); - std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; - std::string label = std::format("[CURRENT CELL] {}", displayName); - - bool isOpen = currentCellLightingWidget && currentCellLightingWidget->IsOpen(); - if (ImGui::Selectable(label.c_str(), isOpen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - // Open or reuse the cell lighting widget + // Special handling for Cell Lighting category + if (selectedCategory == "Cell Lighting") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto cell = player->parentCell; + bool isInterior = cell->IsInteriorCell(); + + if (isInterior) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + // No favorite star for cell lighting (it's always the current cell) + ImGui::Dummy(ImVec2(24, 24)); + + ImGui::TableNextColumn(); + + // Display current cell name + const char* cellName = cell->GetName(); + std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; + std::string label = std::format("[CURRENT CELL] {}", displayName); + + bool isOpen = currentCellLightingWidget && currentCellLightingWidget->IsOpen(); + if (ImGui::Selectable(label.c_str(), isOpen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + // Open or reuse the cell lighting widget + if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { + currentCellLightingWidget->SetOpen(true); + } else { + currentCellLightingWidget = std::make_unique(cell); + currentCellLightingWidget->CacheFormData(); + currentCellLightingWidget->Load(); + currentCellLightingWidget->SetOpen(true); + } + } + } + + // Highlight current cell + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + + // Enter key to open + if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { currentCellLightingWidget->SetOpen(true); - } else { - currentCellLightingWidget = std::make_unique(cell); - currentCellLightingWidget->CacheFormData(); - currentCellLightingWidget->Load(); - currentCellLightingWidget->SetOpen(true); } } - } - - // Highlight current cell - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - - // Enter key to open - if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { - currentCellLightingWidget->SetOpen(true); + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", cell->GetFormID()); + + // File column + ImGui::TableNextColumn(); + auto file = cell->GetFile(0); + if (file) { + ImGui::Text("%s", file->fileName); } + + // Status column + ImGui::TableNextColumn(); + ImGui::Text("Interior Cell"); + } else { + // Show message that cell lighting is only for interior cells + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "Cell Lighting is only available for interior cells."); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "You are currently in an exterior cell."); } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text("0x%08X", cell->GetFormID()); - - // File column - ImGui::TableNextColumn(); - auto file = cell->GetFile(0); - if (file) { - ImGui::Text("%s", file->fileName); - } - - // Status column - ImGui::TableNextColumn(); - ImGui::Text("Interior Cell"); } else { - // Show message that cell lighting is only for interior cells + // No player or cell ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "Cell Lighting is only available for interior cells."); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "You are currently in an exterior cell."); + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Player cell not available."); } - } else { - // No player or cell - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Player cell not available."); } - } - // Get current cell's lighting template for prioritization - RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; - if (selectedCategory == "Lighting Template") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto& cellData = player->parentCell->GetRuntimeData(); - currentCellLightingTemplate = cellData.lightingTemplate; - } - } + // Get current cell's lighting template for prioritization + RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; + if (selectedCategory == "Lighting Template") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto& cellData = player->parentCell->GetRuntimeData(); + currentCellLightingTemplate = cellData.lightingTemplate; + } + } - // Filtered display of widgets - show current cell's lighting template first - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - for (int i = 0; i < sortedWidgets.size(); ++i) { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) - continue; if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + // Filtered display of widgets - show current cell's lighting template first + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + for (int i = 0; i < sortedWidgets.size(); ++i) { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) + continue; + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) continue; // Apply quick filters @@ -440,16 +443,16 @@ void EditorWindow::ShowObjectsWindow() // Editor ID column with [CURRENT] prefix bool isSelected = sortedWidgets[i]->IsOpen(); if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - } - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } // Context menu + if (ImGui::IsMouseDoubleClicked(0)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } + } + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } // Context menu if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; @@ -530,37 +533,37 @@ void EditorWindow::ShowObjectsWindow() AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } } - + // Show ImageSpace and VolumetricLighting info for weather widgets if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { auto* weatherWidget = dynamic_cast(sortedWidgets[i]); if (weatherWidget && weatherWidget->weather) { ImGui::BeginTooltip(); - + // ImageSpace info ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "ImageSpace:"); for (int tod = 0; tod < 4; tod++) { auto imgSpace = weatherWidget->weather->imageSpaces[tod]; - ImGui::Text(" %s: %s", + ImGui::Text(" %s: %s", TOD::GetPeriodName(tod), imgSpace ? imgSpace->GetFormEditorID() : "None"); } - + ImGui::Spacing(); - + // VolumetricLighting info ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Volumetric Lighting:"); for (int tod = 0; tod < 4; tod++) { auto volLight = weatherWidget->weather->volumetricLighting[tod]; - ImGui::Text(" %s: %s", + ImGui::Text(" %s: %s", TOD::GetPeriodName(tod), volLight ? volLight->GetFormEditorID() : "None"); } - + ImGui::EndTooltip(); } } - + // Enter key to open if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { sortedWidgets[i]->SetOpen(true); @@ -692,14 +695,15 @@ void EditorWindow::ShowObjectsWindow() if (markedRecord != settings.markedRecords.end()) { ImGui::Text("%s", markedRecord->second.c_str()); } - } ImGui::EndTable(); } - ImGui::EndTable(); } - // End the window - ImGui::End(); + ImGui::EndTable(); +} + +// End the window +ImGui::End(); } void EditorWindow::ShowViewportWindow() @@ -965,7 +969,7 @@ void EditorWindow::RenderUI() settingsSelectedCategory = "Flags"; } ImGui::Separator(); - + // Current cell lighting auto player = RE::PlayerCharacter::GetSingleton(); if (player && player->parentCell && player->parentCell->IsInteriorCell()) { @@ -976,7 +980,7 @@ void EditorWindow::RenderUI() currentCellLightingWidget->SetOpen(true); found = true; } - + if (!found) { // Create new widget for current cell currentCellLightingWidget = std::make_unique(player->parentCell); @@ -993,9 +997,9 @@ void EditorWindow::RenderUI() ImGui::SetTooltip("Only available in interior cells"); } } - + ImGui::Separator(); - + if (ImGui::Checkbox("Auto-Apply Changes", &settings.autoApplyChanges)) { Save(); } @@ -1020,7 +1024,7 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Palette", nullptr, PaletteWindow::GetSingleton()->open)) { PaletteWindow::GetSingleton()->open = !PaletteWindow::GetSingleton()->open; } - + ImGui::Separator(); ImGui::Text("Open Widgets:"); ImGui::Separator(); @@ -1091,7 +1095,7 @@ void EditorWindow::RenderUI() ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Favorites: %d", (int)settings.favoriteWidgets.size()); - + // Count total recent widgets across all categories int totalRecent = 0; for (const auto& [category, widgets] : settings.recentWidgets) { @@ -1127,48 +1131,48 @@ void EditorWindow::RenderUI() } } - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); + } } - } - // Undo button - if (menu && menu->uiIcons.undo.texture) { - bool canUndo = CanUndo(); + // Undo button + if (menu && menu->uiIcons.undo.texture) { + bool canUndo = CanUndo(); - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - if (!canUndo) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.25f)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.5f)); - } else { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); - } + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + if (!canUndo) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.25f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.5f)); + } else { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + } - const float menuBarHeight = ImGui::GetFrameHeight(); - const float buttonDim = menuBarHeight * 0.85f; - const ImVec2 buttonSize(buttonDim, buttonDim); + const float menuBarHeight = ImGui::GetFrameHeight(); + const float buttonDim = menuBarHeight * 0.85f; + const ImVec2 buttonSize(buttonDim, buttonDim); - if (ImGui::ImageButton("##GlobalUndo", menu->uiIcons.undo.texture, buttonSize) && canUndo) { - PerformUndo(); - } + if (ImGui::ImageButton("##GlobalUndo", menu->uiIcons.undo.texture, buttonSize) && canUndo) { + PerformUndo(); + } - ImGui::PopStyleColor(3); - ImGui::PopStyleVar(); + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(); - if (ImGui::IsItemHovered()) { - if (canUndo) { - ImGui::SetTooltip("Undo (Ctrl+Z) - %d states", (int)undoStack.size()); - } else { - ImGui::SetTooltip("Undo (Ctrl+Z) - No changes to undo"); + if (ImGui::IsItemHovered()) { + if (canUndo) { + ImGui::SetTooltip("Undo (Ctrl+Z) - %d states", (int)undoStack.size()); + } else { + ImGui::SetTooltip("Undo (Ctrl+Z) - No changes to undo"); + } } - } - } // Weather lock indicator + } // Weather lock indicator if (weatherLockActive && lockedWeather) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); @@ -1184,21 +1188,22 @@ void EditorWindow::RenderUI() ImGui::Text(" [TIME PAUSED]"); ImGui::PopStyleColor(); } - - // Close button on the right side - float menuBarHeight = ImGui::GetFrameHeight(); - float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar - ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); - if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { - open = false; - } - ImGui::PopStyleColor(3); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Close Weather Editor (Esc)"); - } ImGui::EndMainMenuBar(); + + // Close button on the right side + float menuBarHeight = ImGui::GetFrameHeight(); + float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar + ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { + open = false; + } + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Close Weather Editor (Esc)"); + } + ImGui::EndMainMenuBar(); } auto width = ImGui::GetIO().DisplaySize.x; @@ -1220,7 +1225,7 @@ void EditorWindow::RenderUI() } ShowWidgetWindow(); - + // Show palette window PaletteWindow::GetSingleton()->Draw(); @@ -1229,7 +1234,7 @@ void EditorWindow::RenderUI() // Pop the alpha style var ImGui::PopStyleVar(); - + // Restore previous font scale io.FontGlobalScale = previousScale; } @@ -1464,26 +1469,26 @@ void EditorWindow::ShowSettingsWindow() ImGui::Checkbox("Use text buttons instead of icons", &settings.useTextButtons); AddTooltip("Display action buttons as text labels instead of icons"); - + ImGui::Checkbox("Enable 'Inherit From Parent' feature", &settings.enableInheritFromParent); AddTooltip("Show checkboxes to copy settings from parent weather (editor-only feature)"); - + ImGui::Separator(); ImGui::TextUnformatted("UI Scale"); ImGui::Spacing(); - + if (ImGui::SliderFloat("Editor UI Scale", &settings.editorUIScale, 0.5f, 2.0f, "%.2f")) { Save(); } AddTooltip("Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)"); - + if (ImGui::Button("Reset to 1.0")) { settings.editorUIScale = 1.0f; Save(); } ImGui::SameLine(); AddTooltip("Reset UI scale to default (100%)"); - + ImGui::Separator(); ImGui::TextUnformatted("Session & History"); ImGui::Spacing(); @@ -1503,7 +1508,7 @@ void EditorWindow::ShowSettingsWindow() settings.favoriteWidgets.clear(); Save(); } - + } else if (settingsSelectedCategory == "Flags") { if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); @@ -1740,7 +1745,7 @@ void EditorWindow::DisableVanityCamera() } } - void EditorWindow::RestoreVanityCamera() +void EditorWindow::RestoreVanityCamera() { if (!vanityCameraDisabled) return; @@ -1873,7 +1878,7 @@ void EditorWindow::RenderNotifications() void EditorWindow::AddToRecent(const std::string& widgetId, const std::string& category) { auto& categoryRecent = settings.recentWidgets[category]; - + // Remove if already exists auto it = std::find(categoryRecent.begin(), categoryRecent.end(), widgetId); if (it != categoryRecent.end()) { diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index e2373ca575..73da6d79e8 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -72,10 +72,9 @@ void WeatherWidget::DrawWidget() ImGui::SetNextWindowFocus(); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.16f, 0.16f, 0.16f, 1.0f)); - if (ImGui::Begin("##SearchDropdown", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { - + if (ImGui::Begin("##SearchDropdown", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { for (size_t i = 0; i < std::min(size_t(5), searchResults.size()); ++i) { const auto& result = searchResults[i]; std::string label = std::format("{} ({})", result.displayName, result.tabName); @@ -107,113 +106,114 @@ void WeatherWidget::DrawWidget() auto& widgets = editorWindow->weatherWidgets; // Sets the parent widget if settings have been loaded. - if (settings.parent != "None") { - parent = GetParent(); - if (parent == nullptr) - settings.parent = "None"; - } - - if (editorWindow->settings.enableInheritFromParent) { - if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { - // Option for "None" - if (ImGui::Selectable("None", parent == nullptr)) { - parent = nullptr; + if (settings.parent != "None") { + parent = GetParent(); + if (parent == nullptr) settings.parent = "None"; - } + } - for (int i = 0; i < widgets.size(); i++) { - auto& widget = widgets[i]; + if (editorWindow->settings.enableInheritFromParent) { + if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { + // Option for "None" + if (ImGui::Selectable("None", parent == nullptr)) { + parent = nullptr; + settings.parent = "None"; + } - // Skip self-selection - if (widget.get() == this) - continue; + for (int i = 0; i < widgets.size(); i++) { + auto& widget = widgets[i]; - // Option for each widget - if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget.get())) { - parent = (WeatherWidget*)widget.get(); - settings.parent = widget->GetEditorID(); - } + // Skip self-selection + if (widget.get() == this) + continue; - // Set default focus to the current parent - if (parent == widget.get()) { - ImGui::SetItemDefaultFocus(); + // Option for each widget + if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget.get())) { + parent = (WeatherWidget*)widget.get(); + settings.parent = widget->GetEditorID(); + } + + // Set default focus to the current parent + if (parent == widget.get()) { + ImGui::SetItemDefaultFocus(); + } } + ImGui::EndCombo(); } - ImGui::EndCombo(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); - ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "Note: This is NOT the same as cell lighting template inheritance."); - ImGui::EndTooltip(); - } - - if (parent) { ImGui::SameLine(); - if (ImGui::Button("Inherit All")) { - InheritAllFromParent(); - } + ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Copy all parameter values from parent weather"); + ImGui::BeginTooltip(); + ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); + ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "Note: This is NOT the same as cell lighting template inheritance."); + ImGui::EndTooltip(); } - - if (!parent->IsOpen()) { + + if (parent) { ImGui::SameLine(); - if (ImGui::Button("Open")) - parent->SetOpen(true); + if (ImGui::Button("Inherit All")) { + InheritAllFromParent(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Copy all parameter values from parent weather"); + } + + if (!parent->IsOpen()) { + ImGui::SameLine(); + if (ImGui::Button("Open")) + parent->SetOpen(true); + } } } - } -} // Tab bar for organizing settings - if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { - // Use activeTabOverride to auto-navigate to specific tab - ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags dalcFlags = (activeTabOverride == "Lighting (DALC)") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags atmosphereFlags = (activeTabOverride == "Atmosphere Colors") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags recordsFlags = (activeTabOverride == "Records") ? ImGuiTabItemFlags_SetSelected : 0; - if (!activeTabOverride.empty()) { - activeTabOverride = ""; // Clear after use - } + } // Tab bar for organizing settings + if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { + // Use activeTabOverride to auto-navigate to specific tab + ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags dalcFlags = (activeTabOverride == "Lighting (DALC)") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags atmosphereFlags = (activeTabOverride == "Atmosphere Colors") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags recordsFlags = (activeTabOverride == "Records") ? ImGuiTabItemFlags_SetSelected : 0; + if (!activeTabOverride.empty()) { + activeTabOverride = ""; // Clear after use + } - if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { - DrawProperties("Sun", { { "Sun Damage", INT8_SLIDER } }); - DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); - DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); - DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, + if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { + DrawProperties("Sun", { { "Sun Damage", INT8_SLIDER } }); + DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); + DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); + DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, { "Thunder Lightning Frequency", INT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); - DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); - DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); - ImGui::EndTabItem(); - } if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { - DrawDALCSettings(); - ImGui::EndTabItem(); - } + DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); + DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { + DrawDALCSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Atmosphere Colors", nullptr, atmosphereFlags)) { - DrawWeatherColorSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Atmosphere Colors", nullptr, atmosphereFlags)) { + DrawWeatherColorSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Clouds", nullptr, cloudsFlags)) { - DrawCloudSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Clouds", nullptr, cloudsFlags)) { + DrawCloudSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Fog", nullptr, fogFlags)) { - DrawFogSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Fog", nullptr, fogFlags)) { + DrawFogSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { - DrawFeatureSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { + DrawFeatureSettings(); + ImGui::EndTabItem(); + } if (ImGui::BeginTabItem("Records", nullptr, recordsFlags)) { ImGui::Spacing(); @@ -223,36 +223,37 @@ void WeatherWidget::DrawWidget() ImGui::Spacing(); auto* editorWindow = EditorWindow::GetSingleton(); - bool recordChanged = false; - bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); - WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; - - // ImageSpace Records (per time of day) - if (ImGui::CollapsingHeader("ImageSpace", ImGuiTreeNodeFlags_DefaultOpen)) { - for (int i = 0; i < ColorTimes::kTotal; i++) { - ImGui::PushID(i); - std::string label = ColorTimeLabel(i); - std::string inheritKey = "ImageSpace_" + std::to_string(i); - - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags[inheritKey]; - if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i]; - recordChanged = true; + bool recordChanged = false; + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; + + // ImageSpace Records (per time of day) + if (ImGui::CollapsingHeader("ImageSpace", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "ImageSpace_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i]; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("%s:", label.c_str()); - ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); + if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->imageSpaces[i]) { ImGui::SameLine(); if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { @@ -267,38 +268,39 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this ImageSpace for editing"); } } - + ImGui::PopID(); } ImGui::Spacing(); } - // Volumetric Lighting Records (per time of day) - if (ImGui::CollapsingHeader("Volumetric Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { - for (int i = 0; i < ColorTimes::kTotal; i++) { - ImGui::PushID(100 + i); - std::string label = ColorTimeLabel(i); - std::string inheritKey = "VolumetricLighting_" + std::to_string(i); - - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags[inheritKey]; - if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i]; - recordChanged = true; + // Volumetric Lighting Records (per time of day) + if (ImGui::CollapsingHeader("Volumetric Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(100 + i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "VolumetricLighting_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i]; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("%s:", label.c_str()); - ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); + if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->volumetricLighting[i]) { ImGui::SameLine(); if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { @@ -313,33 +315,34 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Volumetric Lighting for editing"); } } - + ImGui::PopID(); } ImGui::Spacing(); } - // Precipitation Data - if (ImGui::CollapsingHeader("Precipitation", ImGuiTreeNodeFlags_DefaultOpen)) { - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags["Precipitation"]; - if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->precipitationData = parentWidget->weather->precipitationData; - recordChanged = true; + // Precipitation Data + if (ImGui::CollapsingHeader("Precipitation", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["Precipitation"]; + if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->precipitationData = parentWidget->weather->precipitationData; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("Particle Shader:"); - ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("Particle Shader:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); + if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->precipitationData) { ImGui::SameLine(); if (ImGui::SmallButton("Open##Precip")) { @@ -354,31 +357,32 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Precipitation for editing"); } } - + ImGui::Spacing(); } - // Visual Effect (Reference Effect) - if (ImGui::CollapsingHeader("Visual Effect", ImGuiTreeNodeFlags_DefaultOpen)) { - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags["ReferenceEffect"]; - if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->referenceEffect = parentWidget->weather->referenceEffect; - recordChanged = true; + // Visual Effect (Reference Effect) + if (ImGui::CollapsingHeader("Visual Effect", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["ReferenceEffect"]; + if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->referenceEffect = parentWidget->weather->referenceEffect; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("Reference Effect:"); - ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("Reference Effect:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); + if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->referenceEffect) { ImGui::SameLine(); if (ImGui::SmallButton("Open##RefEffect")) { @@ -393,24 +397,24 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Visual Effect for editing"); } } - - - ImGui::Spacing(); - } - if (recordChanged) { - } + ImGui::Spacing(); + } - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); + if (recordChanged) { + } + + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } ImGui::End(); } void WeatherWidget::LoadSettings() { - bool hadErrors = false; if (!js.empty()) { + bool hadErrors = false; + if (!js.empty()) { try { // Attempt to load settings from JSON settings = js; @@ -466,7 +470,7 @@ void WeatherWidget::SaveSettings() try { js = settings; - + if (js.is_null()) { logger::error("Weather {}: Serialization produced null JSON!", GetEditorID()); } else if (!js.contains("weatherProperties")) { @@ -577,7 +581,7 @@ void WeatherWidget::SetWeatherValues() weather->cloudLayerSpeedX[i] = (int8_t)settingsCloud.cloudLayerSpeedX; weather->cloudLayerSpeedY[i] = (int8_t)settingsCloud.cloudLayerSpeedY; - + if (!settingsCloud.enabled) { disabledBits |= (1 << i); } @@ -870,23 +874,23 @@ void WeatherWidget::DrawWeatherColorSettings() // Organized display order: group related sky/fog/lighting properties static const int displayOrder[] = { - 0, // Sky Upper - 7, // Sky Lower - 8, // Horizon - 1, // Fog Near - 12, // Fog Far - 3, // Ambient - 4, // Sunlight - 15, // Sun Glare - 5, // Sun - 6, // Stars - 16, // Moon Glare - 9, // Effect Lighting - 10, // Cloud LOD Diffuse - 11, // Cloud LOD Ambient - 13, // Sky Statics - 14, // Water Multiplier - 2, // Unknown + 0, // Sky Upper + 7, // Sky Lower + 8, // Horizon + 1, // Fog Near + 12, // Fog Far + 3, // Ambient + 4, // Sunlight + 15, // Sun Glare + 5, // Sun + 6, // Stars + 16, // Moon Glare + 9, // Effect Lighting + 10, // Cloud LOD Diffuse + 11, // Cloud LOD Ambient + 13, // Sky Statics + 14, // Water Multiplier + 2, // Unknown }; for (int idx = 0; idx < ColorTypes::kTotal; idx++) { @@ -926,73 +930,76 @@ void WeatherWidget::DrawCloudSettings() bool changed = false; for (int i = 0; i < TESWeather::kTotalLayers; i++) { std::string layer = std::format("Layer {}", i); - + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (settings.clouds[i].enabled && !settings.clouds[i].texturePath.empty()) { flags |= ImGuiTreeNodeFlags_DefaultOpen; } - if (ImGui::CollapsingHeader(layer.c_str(), flags)) { - ImGui::Indent(10.0f); - ImGui::Spacing(); - - bool layerEnabled = settings.clouds[i].enabled; - - // Begin horizontal layout for enable checkbox and sliders on left, texture on right - ImGui::BeginGroup(); - - if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { - settings.clouds[i].enabled = layerEnabled; - if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { - EditorWindow::GetSingleton()->PushUndoState(this); - ApplyChanges(); + if (ImGui::CollapsingHeader(layer.c_str(), flags)) { + ImGui::Indent(10.0f); + ImGui::Spacing(); + + bool layerEnabled = settings.clouds[i].enabled; + + // Begin horizontal layout for enable checkbox and sliders on left, texture on right + ImGui::BeginGroup(); + + if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { + settings.clouds[i].enabled = layerEnabled; + if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { + EditorWindow::GetSingleton()->PushUndoState(this); + ApplyChanges(); + } + changed = true; } - changed = true; - } - - ImGui::Spacing(); - ImGui::Spacing(); - - // Make sliders 1/3 width - ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.4f); - if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; - ImGui::Spacing(); - if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; - ImGui::PopItemWidth(); - - ImGui::EndGroup(); - - // Draw texture in upper right if available - if (!settings.clouds[i].texturePath.empty()) { - auto* texture = GetCloudTexture(i); - if (texture) { - ImGui::SameLine(0.0f, 20.0f); - ImGui::BeginGroup(); - float textureSize = 128.0f; - ImGui::Image((void*)texture, ImVec2(textureSize, textureSize)); - // Small grey subtext below image - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); - ImGui::PushFont(ImGui::GetFont()); - ImGui::SetWindowFontScale(0.8f); - float textWidth = ImGui::CalcTextSize(settings.clouds[i].texturePath.c_str()).x; - if (textWidth > textureSize) { - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + textureSize); - ImGui::TextWrapped("%s", settings.clouds[i].texturePath.c_str()); - ImGui::PopTextWrapPos(); - } else { - ImGui::Text("%s", settings.clouds[i].texturePath.c_str()); + + ImGui::Spacing(); + ImGui::Spacing(); + + // Make sliders 1/3 width + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.4f); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) + changed = true; + ImGui::Spacing(); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) + changed = true; + ImGui::PopItemWidth(); + + ImGui::EndGroup(); + + // Draw texture in upper right if available + if (!settings.clouds[i].texturePath.empty()) { + auto* texture = GetCloudTexture(i); + if (texture) { + ImGui::SameLine(0.0f, 20.0f); + ImGui::BeginGroup(); + float textureSize = 128.0f; + ImGui::Image((void*)texture, ImVec2(textureSize, textureSize)); + // Small grey subtext below image + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushFont(ImGui::GetFont()); + ImGui::SetWindowFontScale(0.8f); + float textWidth = ImGui::CalcTextSize(settings.clouds[i].texturePath.c_str()).x; + if (textWidth > textureSize) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + textureSize); + ImGui::TextWrapped("%s", settings.clouds[i].texturePath.c_str()); + ImGui::PopTextWrapPos(); + } else { + ImGui::Text("%s", settings.clouds[i].texturePath.c_str()); + } + ImGui::SetWindowFontScale(1.0f); + ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::EndGroup(); } - ImGui::SetWindowFontScale(1.0f); - ImGui::PopFont(); - ImGui::PopStyleColor(); - ImGui::EndGroup(); } - } - - ImGui::Spacing(); - ImGui::Spacing(); if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { + + ImGui::Spacing(); + ImGui::Spacing(); + if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { TOD::RenderTODHeader(); - + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Separator(); @@ -1029,7 +1036,7 @@ void WeatherWidget::DrawCloudSettings() TOD::EndTODTable(); } - + ImGui::Spacing(); ImGui::Unindent(10.0f); } @@ -1052,7 +1059,7 @@ void WeatherWidget::DrawFogSettings() ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthFixed, 250.0f); ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthFixed, 250.0f); - + // Header row ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -1060,7 +1067,7 @@ void WeatherWidget::DrawFogSettings() ImGui::Text("Day"); ImGui::TableSetColumnIndex(2); ImGui::Text("Night"); - + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Separator(); @@ -1200,7 +1207,7 @@ void WeatherWidget::DrawProperties(std::string category, std::map(ImGui::GetTime()) - highlightStartTime; @@ -1220,7 +1227,7 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.weatherProperties[property]; } else if (settings.weatherColors.find(property) != settings.weatherColors.end()) { @@ -1285,41 +1298,43 @@ void WeatherWidget::InheritFromParent(const std::string& property) void WeatherWidget::InheritAllFromParent() { - if (!HasParent()) return; - + if (!HasParent()) + return; + WeatherWidget* parentWidget = GetParent(); - if (!parentWidget) return; - + if (!parentWidget) + return; + // Copy all weather properties for (const auto& [key, value] : parentWidget->settings.weatherProperties) { settings.weatherProperties[key] = value; } - + // Copy all weather colors for (const auto& [key, value] : parentWidget->settings.weatherColors) { settings.weatherColors[key] = value; } - + // Copy all fog properties for (const auto& [key, value] : parentWidget->settings.fogProperties) { settings.fogProperties[key] = value; } - + // Copy atmosphere colors for (int i = 0; i < ColorTypes::kTotal; i++) { settings.atmosphereColors[i] = parentWidget->settings.atmosphereColors[i]; } - + // Copy DALC settings for (int i = 0; i < ColorTimes::kTotal; i++) { settings.dalc[i] = parentWidget->settings.dalc[i]; } - + // Copy cloud settings for (int i = 0; i < TESWeather::kTotalLayers; i++) { settings.clouds[i] = parentWidget->settings.clouds[i]; } - + // Set all inherit flags to true settings.inheritFlags["DALC_Specular"] = true; settings.inheritFlags["DALC_Fresnel"] = true; @@ -1329,12 +1344,12 @@ void WeatherWidget::InheritAllFromParent() settings.inheritFlags["DALC_DirYMin"] = true; settings.inheritFlags["DALC_DirZMax"] = true; settings.inheritFlags["DALC_DirZMin"] = true; - + settings.inheritFlags["Fog_Near"] = true; settings.inheritFlags["Fog_Far"] = true; settings.inheritFlags["Fog_Power"] = true; settings.inheritFlags["Fog_Max"] = true; - + // Atmosphere colors static const int displayOrder[] = { 0, 7, 8, 1, 12, 3, 4, 5, 6, 9, 10, 11, 13, 14, 15, 16, 2 }; for (int idx = 0; idx < ColorTypes::kTotal; idx++) { @@ -1342,18 +1357,18 @@ void WeatherWidget::InheritAllFromParent() std::string colorTypeLabel = ColorTypeLabel(i); settings.inheritFlags["Atmosphere_" + colorTypeLabel] = true; } - + // Cloud settings for (int i = 0; i < TESWeather::kTotalLayers; i++) { settings.inheritFlags[std::format("Cloud{}_Color", i)] = true; settings.inheritFlags[std::format("Cloud{}_Alpha", i)] = true; } - + // Apply the changes if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } - + EditorWindow::GetSingleton()->ShowNotification( std::format("Inherited all settings from {}", parentWidget->GetEditorID()), ImVec4(0.0f, 1.0f, 0.5f, 1.0f), @@ -1405,7 +1420,8 @@ void WeatherWidget::LoadFeatureSettings() if (!missingFeatures.empty()) { std::string missingList; for (size_t i = 0; i < missingFeatures.size(); ++i) { - if (i > 0) missingList += ", "; + if (i > 0) + missingList += ", "; missingList += missingFeatures[i]; } @@ -1626,21 +1642,19 @@ ID3D11ShaderResourceView* WeatherWidget::GetCloudTexture(int layerIndex) // Build resource path for BSA loading: Textures\path (relative to Data folder) // Note: Skyrim texture paths don't include .dds extension, so we add it std::string resourcePath = std::string("Textures\\") + texturePath; - + // Add .dds extension if not present if (resourcePath.size() < 4 || resourcePath.substr(resourcePath.size() - 4) != ".dds") { resourcePath += ".dds"; } - + ID3D11ShaderResourceView* srv = nullptr; ImVec2 textureSize; - + if (Util::LoadDDSTextureFromFile(globals::d3d::device, resourcePath.c_str(), &srv, textureSize)) { cloudTextureCache[layerIndex] = srv; return srv; } - + return nullptr; } - - From f31eb1deef8bbd8b4c1b019111edd3e4e91708ae Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 10 Dec 2025 13:45:40 +1000 Subject: [PATCH 12/30] fixes --- src/WeatherEditor/EditorWindow.cpp | 448 ++++++++---------- .../Weather/CellLightingWidget.cpp | 1 + .../Weather/ImageSpaceWidget.cpp | 3 +- .../Weather/LightingTemplateWidget.cpp | 1 + .../Weather/PrecipitationWidget.cpp | 1 + .../Weather/VolumetricLightingWidget.cpp | 1 + src/WeatherEditor/Weather/WeatherWidget.cpp | 14 +- src/WeatherEditor/WeatherUtils.cpp | 146 +++++- src/WeatherEditor/WeatherUtils.h | 10 +- 9 files changed, 364 insertions(+), 261 deletions(-) diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index a04dad8f26..e0358f2b3f 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1,6 +1,7 @@ #include "EditorWindow.h" #include "Features/WeatherEditor.h" +#include "Menu.h" #include "State.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" @@ -168,11 +169,11 @@ void EditorWindow::ShowObjectsWindow() auto sky = globals::game::sky; if (sky && sky->currentWeather) { auto currentWeather = sky->currentWeather; - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetTheme().StatusPalette.RestartNeeded); ImGui::Text("Current Active Weather:"); ImGui::PopStyleColor(); ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", currentWeather->GetFormEditorID()); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().Palette.Text, "%s", currentWeather->GetFormEditorID()); ImGui::SameLine(); ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); @@ -224,7 +225,7 @@ void EditorWindow::ShowObjectsWindow() auto recentIt = settings.recentWidgets.find(selectedCategory); if (recentIt != settings.recentWidgets.end() && !recentIt->second.empty()) { ImGui::Spacing(); - ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Recent:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Recent:"); ImGui::SameLine(); for (size_t i = 0; i < std::min(size_t(5), recentIt->second.size()); ++i) { if (i > 0) ImGui::SameLine(); @@ -357,8 +358,10 @@ void EditorWindow::ShowObjectsWindow() } // Highlight current cell - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); + auto highlightColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor; + highlightColor.w = 0.3f; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(highlightColor)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(highlightColor)); // Enter key to open if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { @@ -385,14 +388,14 @@ void EditorWindow::ShowObjectsWindow() // Show message that cell lighting is only for interior cells ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "Cell Lighting is only available for interior cells."); - ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "You are currently in an exterior cell."); + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Warning, "Cell Lighting is only available for interior cells."); + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable, "You are currently in an exterior cell."); } } else { // No player or cell ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); - ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "Player cell not available."); + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Error, "Player cell not available."); } } @@ -411,209 +414,8 @@ void EditorWindow::ShowObjectsWindow() for (int i = 0; i < sortedWidgets.size(); ++i) { auto* ltWidget = dynamic_cast(sortedWidgets[i]); if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) - continue; if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) - continue; - - // Apply quick filters - if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) - continue; - if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) - continue; - - auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); - auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); - ImGui::TableNextRow(); - - // Highlight current cell's lighting template - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(ImVec4(0.2f, 0.4f, 0.6f, 0.3f))); - - ImGui::TableSetColumnIndex(0); - - // Favorite star - if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { - ToggleFavorite(sortedWidgets[i]->GetEditorID()); - } - - ImGui::TableNextColumn(); - - // Editor ID column with [CURRENT] prefix - bool isSelected = sortedWidgets[i]->IsOpen(); - if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - } - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } // Context menu - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { - auto& markedRecords = settings.markedRecords; - - for (auto& recordMarker : settings.recordMarkers) { - if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; - Save(); - } - } - - if (ImGui::MenuItem("Remove")) { - markedRecords.erase(sortedWidgets[i]->GetEditorID()); - Save(); - } - - ImGui::EndPopup(); - } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); - - // File column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); - - // Status column - ImGui::TableNextColumn(); - if (markedRecord != settings.markedRecords.end()) { - ImGui::Text("%s", markedRecord->second.c_str()); - } - } - } - - // Filtered display of widgets - regular list - for (int i = 0; i < sortedWidgets.size(); ++i) { - // Skip current cell's lighting template if already shown - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) - continue; - } - - if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) - continue; - - // Apply quick filters - if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) - continue; - if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) - continue; - - auto editorLabel = sortedWidgets[i]->GetEditorID(); - auto markedRecord = settings.markedRecords.find(editorLabel); - ImGui::TableNextRow(); - - // Set background colour - if (markedRecord != settings.markedRecords.end()) { - auto& color = settings.recordMarkers[markedRecord->second]; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); - } - - ImGui::TableSetColumnIndex(0); - - // Favorite star - if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { - ToggleFavorite(sortedWidgets[i]->GetEditorID()); - } - - ImGui::TableNextColumn(); - - // Editor ID column - bool isSelected = sortedWidgets[i]->IsOpen(); - if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - } - - // Show ImageSpace and VolumetricLighting info for weather widgets - if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { - auto* weatherWidget = dynamic_cast(sortedWidgets[i]); - if (weatherWidget && weatherWidget->weather) { - ImGui::BeginTooltip(); - - // ImageSpace info - ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "ImageSpace:"); - for (int tod = 0; tod < 4; tod++) { - auto imgSpace = weatherWidget->weather->imageSpaces[tod]; - ImGui::Text(" %s: %s", - TOD::GetPeriodName(tod), - imgSpace ? imgSpace->GetFormEditorID() : "None"); - } - - ImGui::Spacing(); - - // VolumetricLighting info - ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Volumetric Lighting:"); - for (int tod = 0; tod < 4; tod++) { - auto volLight = weatherWidget->weather->volumetricLighting[tod]; - ImGui::Text(" %s: %s", - TOD::GetPeriodName(tod), - volLight ? volLight->GetFormEditorID() : "None"); - } - - ImGui::EndTooltip(); - } - } - - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - - // Opens a context menu on right click to mark records by color - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { - auto& markedRecords = settings.markedRecords; - - for (auto& recordMarker : settings.recordMarkers) { - if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[editorLabel] = recordMarker.first; - Save(); - } - } - - if (ImGui::MenuItem("Remove")) { - markedRecords.erase(editorLabel); - Save(); - } - - ImGui::EndPopup(); - } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); - - // File column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); - - // Status column - ImGui::TableNextColumn(); - - // Re-check if the record exists after potential removal - markedRecord = settings.markedRecords.find(editorLabel); - if (markedRecord != settings.markedRecords.end()) { - ImGui::Text("%s", markedRecord->second.c_str()); - } - } - } - - // Filtered display of widgets - regular list - for (int i = 0; i < sortedWidgets.size(); ++i) { - // Skip current cell's lighting template if already shown - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) - continue; - } - + continue; + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) continue; @@ -623,27 +425,26 @@ void EditorWindow::ShowObjectsWindow() if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) continue; - auto editorLabel = sortedWidgets[i]->GetEditorID(); - auto markedRecord = settings.markedRecords.find(editorLabel); + auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); + auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); ImGui::TableNextRow(); - // Set background colour - if (markedRecord != settings.markedRecords.end()) { - auto& color = settings.recordMarkers[markedRecord->second]; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); - } + // Highlight current cell's lighting template + auto highlightColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor; + highlightColor.w = 0.3f; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(highlightColor)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(highlightColor)); ImGui::TableSetColumnIndex(0); // Favorite star - if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { ToggleFavorite(sortedWidgets[i]->GetEditorID()); } ImGui::TableNextColumn(); - // Editor ID column + // Editor ID column with [CURRENT] prefix bool isSelected = sortedWidgets[i]->IsOpen(); if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { if (ImGui::IsMouseDoubleClicked(0)) { @@ -651,25 +452,26 @@ void EditorWindow::ShowObjectsWindow() AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } } + // Enter key to open if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { sortedWidgets[i]->SetOpen(true); AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } - - // Opens a context menu on right click to mark records by color + + // Context menu if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { auto& markedRecords = settings.markedRecords; for (auto& recordMarker : settings.recordMarkers) { if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[editorLabel] = recordMarker.first; + settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; Save(); } } if (ImGui::MenuItem("Remove")) { - markedRecords.erase(editorLabel); + markedRecords.erase(sortedWidgets[i]->GetEditorID()); Save(); } @@ -686,18 +488,138 @@ void EditorWindow::ShowObjectsWindow() // Status column ImGui::TableNextColumn(); - - // Re-check if the record exists after potential removal - markedRecord = settings.markedRecords.find(editorLabel); if (markedRecord != settings.markedRecords.end()) { ImGui::Text("%s", markedRecord->second.c_str()); } - } ImGui::EndTable(); } + } - ImGui::EndTable(); + // Filtered display of widgets - regular list + for (int i = 0; i < sortedWidgets.size(); ++i) { + // Skip current cell's lighting template if already shown + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) + continue; + } + + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + continue; + + // Apply quick filters + if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) + continue; + if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) + continue; + + auto editorLabel = sortedWidgets[i]->GetEditorID(); + auto markedRecord = settings.markedRecords.find(editorLabel); + ImGui::TableNextRow(); + + // Set background colour + if (markedRecord != settings.markedRecords.end()) { + auto& color = settings.recordMarkers[markedRecord->second]; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); + } + + ImGui::TableSetColumnIndex(0); + + // Favorite star + if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + ToggleFavorite(sortedWidgets[i]->GetEditorID()); + } + + ImGui::TableNextColumn(); + + // Editor ID column + bool isSelected = sortedWidgets[i]->IsOpen(); + if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } + } + + // Show ImageSpace and VolumetricLighting info for weather widgets + if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { + auto* weatherWidget = dynamic_cast(sortedWidgets[i]); + if (weatherWidget && weatherWidget->weather) { + ImGui::BeginTooltip(); + + // ImageSpace info + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "ImageSpace:"); + for (int tod = 0; tod < 4; tod++) { + auto imgSpace = weatherWidget->weather->imageSpaces[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + imgSpace ? imgSpace->GetFormEditorID() : "None"); + } + + ImGui::Spacing(); + + // VolumetricLighting info + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "Volumetric Lighting:"); + for (int tod = 0; tod < 4; tod++) { + auto volLight = weatherWidget->weather->volumetricLighting[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + volLight ? volLight->GetFormEditorID() : "None"); + } + + ImGui::EndTooltip(); + } + } + + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } + + // Opens a context menu on right click to mark records by color + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; + + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[editorLabel] = recordMarker.first; + Save(); + } + } + + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(editorLabel); + Save(); + } + + ImGui::EndPopup(); + } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); + + // File column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); + + // Status column + ImGui::TableNextColumn(); + + // Re-check if the record exists after potential removal + markedRecord = settings.markedRecords.find(editorLabel); + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text("%s", markedRecord->second.c_str()); + } } + ImGui::EndTable(); // End DetailsTable + } // End if BeginTable("DetailsTable") + + ImGui::EndTable(); // End ObjectTable + } // End if BeginTable("ObjectTable") + // End the window ImGui::End(); } @@ -1068,14 +990,14 @@ void EditorWindow::RenderUI() if (ImGui::BeginMenu("Help")) { ImGui::Text("Weather Editor"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Keyboard Shortcuts:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Keyboard Shortcuts:"); ImGui::BulletText("Ctrl+F: Focus search"); ImGui::BulletText("Ctrl+S: Save all open widgets"); ImGui::BulletText("Ctrl+W: Close focused widget"); ImGui::BulletText("Enter: Open selected widget"); ImGui::BulletText("Esc: Close editor"); ImGui::Separator(); - ImGui::TextColored(ImVec4(0.7f, 0.9f, 1.0f, 1.0f), "Quick Tips:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Quick Tips:"); ImGui::BulletText("Double-click to edit"); ImGui::BulletText("Right-click to mark status"); ImGui::BulletText("Click star icon to favorite"); @@ -1090,14 +1012,14 @@ void EditorWindow::RenderUI() ImGui::BulletText("Lighting: %d", (int)lightingTemplateWidgets.size()); ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); ImGui::Separator(); - ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Favorites: %d", (int)settings.favoriteWidgets.size()); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.CurrentHotkey, "Favorites: %d", (int)settings.favoriteWidgets.size()); // Count total recent widgets across all categories int totalRecent = 0; for (const auto& [category, widgets] : settings.recentWidgets) { totalRecent += static_cast(widgets.size()); } - ImGui::TextColored(ImVec4(0.7f, 1.0f, 0.7f, 1.0f), "Recent: %d", totalRecent); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.SuccessColor, "Recent: %d", totalRecent); ImGui::EndMenu(); } @@ -1108,11 +1030,18 @@ void EditorWindow::RenderUI() ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); if (isPaused) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.1f, 0.5f, 0.1f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.15f, 0.6f, 0.15f, 1.0f)); + auto pausedColor = Menu::GetSingleton()->GetTheme().StatusPalette.SuccessColor; + pausedColor.w = 0.6f; + auto pausedHoverColor = pausedColor; + pausedHoverColor.w = 0.8f; + ImGui::PushStyleColor(ImGuiCol_Button, pausedColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, pausedHoverColor); } else { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); + auto transparentColor = ImVec4(0, 0, 0, 0); + ImGui::PushStyleColor(ImGuiCol_Button, transparentColor); + auto hoverColor = Menu::GetSingleton()->GetSettings().Theme.Palette.Text; + hoverColor.w = 0.25f; + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoverColor); } const float menuBarHeight = ImGui::GetFrameHeight(); @@ -1141,13 +1070,21 @@ void EditorWindow::RenderUI() ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); if (!canUndo) { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.25f)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 0.5f)); + auto transparentColor = ImVec4(0, 0, 0, 0); + ImGui::PushStyleColor(ImGuiCol_Button, transparentColor); + auto disabledColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable; + disabledColor.w = 0.25f; + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, disabledColor); + auto disabledTextColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable; + disabledTextColor.w = 0.5f; + ImGui::PushStyleColor(ImGuiCol_Text, disabledTextColor); } else { - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.8f, 0.8f, 0.25f)); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + auto transparentColor = ImVec4(0, 0, 0, 0); + ImGui::PushStyleColor(ImGuiCol_Button, transparentColor); + auto hoverColor = Menu::GetSingleton()->GetSettings().Theme.Palette.Text; + hoverColor.w = 0.25f; + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoverColor); + ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetSettings().Theme.Palette.Text); } const float menuBarHeight = ImGui::GetFrameHeight(); @@ -1171,7 +1108,7 @@ void EditorWindow::RenderUI() } // Weather lock indicator if (weatherLockActive && lockedWeather) { ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetSettings().Theme.StatusPalette.SuccessColor); const char* weatherName = lockedWeather->GetFormEditorID(); ImGui::Text(" [LOCKED: %s]", weatherName ? weatherName : "Unknown"); ImGui::PopStyleColor(); @@ -1180,7 +1117,7 @@ void EditorWindow::RenderUI() // Time pause indicator if (timePaused) { ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetSettings().Theme.StatusPalette.CurrentHotkey); ImGui::Text(" [TIME PAUSED]"); ImGui::PopStyleColor(); } @@ -1189,9 +1126,16 @@ void EditorWindow::RenderUI() float menuBarHeight = ImGui::GetFrameHeight(); float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.7f, 0.1f, 0.1f, 1.0f)); + auto errorColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Error; + auto errorHoverColor = errorColor; + errorHoverColor.x = std::min(1.0f, errorColor.x * 1.2f); + errorHoverColor.y = std::min(1.0f, errorColor.y * 0.75f); + auto errorActiveColor = errorColor; + errorActiveColor.x = std::max(0.0f, errorColor.x * 0.875f); + errorActiveColor.y = std::max(0.0f, errorColor.y * 0.25f); + ImGui::PushStyleColor(ImGuiCol_Button, errorColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, errorHoverColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, errorActiveColor); if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { open = false; } @@ -1547,7 +1491,9 @@ void EditorWindow::ShowSettingsWindow() } ImGui::TableSetColumnIndex(2); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); + auto deleteColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Warning; + deleteColor.y = deleteColor.y * 0.5f; + ImGui::PushStyleColor(ImGuiCol_Button, deleteColor); if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { markerToDelete = recordMarker.first; } @@ -1809,7 +1755,7 @@ void EditorWindow::PerformUndo() state.widget->ApplyChanges(); ShowNotification( std::format("Undone changes to {}", state.widgetId), - ImVec4(0.3f, 0.8f, 1.0f, 1.0f), + Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, 2.0f); } } diff --git a/src/WeatherEditor/Weather/CellLightingWidget.cpp b/src/WeatherEditor/Weather/CellLightingWidget.cpp index 545443a61c..57513fcd7d 100644 --- a/src/WeatherEditor/Weather/CellLightingWidget.cpp +++ b/src/WeatherEditor/Weather/CellLightingWidget.cpp @@ -4,6 +4,7 @@ void CellLightingWidget::DrawWidget() { + WeatherUtils::SetCurrentWidget(this); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { DrawWidgetHeader("##CellLightingSearch", false, true); diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp index 430d63c26e..fe700230a9 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -29,6 +29,7 @@ ImageSpaceWidget::~ImageSpaceWidget() void ImageSpaceWidget::DrawWidget() { + WeatherUtils::SetCurrentWidget(this); auto editorWindow = EditorWindow::GetSingleton(); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); @@ -156,7 +157,7 @@ void ImageSpaceWidget::DrawWidget() ImGui::Text("Tint Color"); ImGui::TableSetColumnIndex(1); ImGui::SetNextItemWidth(-1); - if (ImGui::ColorEdit3("##TintColor", &settings.tintColor.x)) + if (WeatherUtils::DrawColorEdit("Tint Color", settings.tintColor)) changed = true; } diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index a6c3b68734..5d9b32aae2 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -28,6 +28,7 @@ LightingTemplateWidget::~LightingTemplateWidget() void LightingTemplateWidget::DrawWidget() { + WeatherUtils::SetCurrentWidget(this); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { // Draw header with search and Save/Load/Delete buttons diff --git a/src/WeatherEditor/Weather/PrecipitationWidget.cpp b/src/WeatherEditor/Weather/PrecipitationWidget.cpp index 7a4bf19435..8914060b73 100644 --- a/src/WeatherEditor/Weather/PrecipitationWidget.cpp +++ b/src/WeatherEditor/Weather/PrecipitationWidget.cpp @@ -4,6 +4,7 @@ void PrecipitationWidget::DrawWidget() { + WeatherUtils::SetCurrentWidget(this); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { DrawWidgetHeader("##PrecipitationSearch", false, true); diff --git a/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp b/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp index 06b5cdc206..f5cfc5f020 100644 --- a/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp +++ b/src/WeatherEditor/Weather/VolumetricLightingWidget.cpp @@ -4,6 +4,7 @@ void VolumetricLightingWidget::DrawWidget() { + WeatherUtils::SetCurrentWidget(this); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { DrawWidgetHeader("##VolumetricLightingSearch", false, true); diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index e2373ca575..477db683e0 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -54,6 +54,7 @@ WeatherWidget::~WeatherWidget() void WeatherWidget::DrawWidget() { + WeatherUtils::SetCurrentWidget(this); ImGui::SetNextWindowSizeConstraints(ImVec2(600, 0), ImVec2(FLT_MAX, FLT_MAX)); if (ImGui::Begin(GetEditorID().c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) { // Draw header with search and all buttons @@ -943,10 +944,17 @@ void WeatherWidget::DrawCloudSettings() if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { settings.clouds[i].enabled = layerEnabled; - if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { - EditorWindow::GetSingleton()->PushUndoState(this); - ApplyChanges(); + // Always apply cloud enable/disable immediately for instant feedback + EditorWindow::GetSingleton()->PushUndoState(this); + ApplyChanges(); + + // Force weather re-application if locked to make cloud changes visible immediately + if (editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather) { + if (auto sky = RE::Sky::GetSingleton()) { + sky->ForceWeather(weather, false); + } } + changed = true; } diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index ff4482a75e..8fb43ebf06 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -2,6 +2,9 @@ #include "EditorWindow.h" #include "PaletteWindow.h" +// Global widget context for undo tracking +static Widget* g_currentWidget = nullptr; + bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring) { if (a_substring.empty()) @@ -146,13 +149,46 @@ std::string ColorTypeLabel(const int i) namespace WeatherUtils { + void SetCurrentWidget(Widget* widget) + { + g_currentWidget = widget; + } + bool DrawSliderInt8(const std::string& label, int& property) { static std::map pendingValues; static std::map lastChangeTime; + static std::map wasActive; + static std::map undoPushedForSession; const double debounceDelay = 2.0; + // Check if item was active in previous frame + bool isPreviouslyActive = wasActive[label]; + bool changed = ImGui::SliderInt(label.c_str(), &property, -128, 127); + + // Check if item is now active + bool isNowActive = ImGui::IsItemActive(); + + // Push undo state only once when slider becomes active (not every frame while dragging) + if (isNowActive && !isPreviouslyActive && !undoPushedForSession[label]) { + if (g_currentWidget) { + EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); + undoPushedForSession[label] = true; + } + } + + // Reset undo flag when slider is completely released and idle for a while + if (!isNowActive && undoPushedForSession[label]) { + if (lastChangeTime.find(label) == lastChangeTime.end() || + ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { + undoPushedForSession[label] = false; + } + } + + // Update active state for next frame + wasActive[label] = isNowActive; + if (changed) { pendingValues[label] = property; lastChangeTime[label] = ImGui::GetTime(); @@ -176,7 +212,7 @@ namespace WeatherUtils return changed; } - bool DrawColorEdit(const std::string& l, float3& property) + bool DrawColorEdit(const std::string& l, float3& property, Widget* widget) { static std::map colorCache; static std::string activeColorId; @@ -186,10 +222,15 @@ namespace WeatherUtils bool isActive = ImGui::IsPopupOpen(l.c_str(), ImGuiPopupFlags_AnyPopupId); bool wasActive = wasPickerOpen[cacheId]; - // Cache the original color when picker is first activated + // Cache the original color and push undo state when picker is first activated if (isActive && activeColorId != cacheId) { colorCache[cacheId] = property; activeColorId = cacheId; + // Push undo state before change (prefer parameter, fallback to global) + Widget* w = widget ? widget : g_currentWidget; + if (w) { + EditorWindow::GetSingleton()->PushUndoState(w); + } } else if (!isActive && activeColorId == cacheId) { activeColorId.clear(); } @@ -239,13 +280,44 @@ namespace WeatherUtils return ImGui::SliderInt(label.c_str(), &property, 0, 255); } - bool DrawSliderFloat(const std::string& label, float& property, float min, float max) + bool DrawSliderFloat(const std::string& label, float& property, float min, float max, Widget* widget) { static std::map pendingValues; static std::map lastChangeTime; + static std::map wasActive; + static std::map undoPushedForSession; const double debounceDelay = 2.0; + // Check if item was active in previous frame + bool isPreviouslyActive = wasActive[label]; + bool changed = ImGui::SliderFloat(label.c_str(), &property, min, max); + + // Check if item is now active + bool isNowActive = ImGui::IsItemActive(); + + // Push undo state only once when slider becomes active (not every frame while dragging) + if (isNowActive && !isPreviouslyActive && !undoPushedForSession[label]) { + // Use parameter if provided, otherwise use global widget + Widget* w = widget ? widget : g_currentWidget; + if (w) { + EditorWindow::GetSingleton()->PushUndoState(w); + undoPushedForSession[label] = true; + } + } + + // Reset undo flag when slider is completely released and idle for a while + if (!isNowActive && undoPushedForSession[label]) { + // Allow new undo push after slider has been released + if (lastChangeTime.find(label) == lastChangeTime.end() || + ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { + undoPushedForSession[label] = false; + } + } + + // Update active state for next frame + wasActive[label] = isNowActive; + if (changed) { pendingValues[label] = property; lastChangeTime[label] = ImGui::GetTime(); @@ -526,6 +598,13 @@ namespace TOD bool isPopupOpen = ImGui::BeginPopup(id.c_str()); bool wasOpen = wasPopupOpen[id]; + // Push undo state when popup first opens + if (isPopupOpen && !wasOpen) { + if (g_currentWidget) { + EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); + } + } + if (isPopupOpen) { // Check for Ctrl+Z while picker is active if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_Z)) { @@ -563,6 +642,8 @@ namespace TOD { static std::map pendingSliderValues; static std::map sliderLastChangeTime; + static std::map wasActiveInherit; + static std::map undoPushedInherit; const double debounceDelay = 2.0; float factors[4]; @@ -613,6 +694,9 @@ namespace TOD ImGui::PushItemWidth(sliderWidth); std::string id = std::string("##") + label + std::to_string(i); + std::string itemKey = std::string(label) + "_slider_" + std::to_string(i); + bool isPreviouslyActive = wasActiveInherit[itemKey]; + ImGui::BeginDisabled(inheritFlags && inheritFlags[i]); if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { changed = true; @@ -622,6 +706,23 @@ namespace TOD pendingSliderValues[valueName] = values[i]; sliderLastChangeTime[valueName] = ImGui::GetTime(); } + + // Push undo state only once when slider becomes active + bool isNowActive = ImGui::IsItemActive(); + if (isNowActive && !isPreviouslyActive && !undoPushedInherit[itemKey]) { + if (g_currentWidget) { + EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); + undoPushedInherit[itemKey] = true; + } + } + + // Reset undo flag when slider is released + if (!isNowActive && undoPushedInherit[itemKey]) { + undoPushedInherit[itemKey] = false; + } + + wasActiveInherit[itemKey] = isNowActive; + ImGui::EndDisabled(); if (ImGui::IsItemHovered()) @@ -763,7 +864,18 @@ namespace TOD } // Color picker popup - if (ImGui::BeginPopup(id.c_str())) { + static std::map wasPopupOpenInherit; + bool isPopupOpen = ImGui::BeginPopup(id.c_str()); + bool wasOpen = wasPopupOpenInherit[id]; + + // Push undo state when popup first opens + if (isPopupOpen && !wasOpen) { + if (g_currentWidget) { + EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); + } + } + + if (isPopupOpen) { if (colorCache.find(id) == colorCache.end()) { colorCache[id] = colors[i]; } @@ -783,6 +895,8 @@ namespace TOD activeColorId = ""; } } + + wasPopupOpenInherit[id] = isPopupOpen; ImGui::EndDisabled(); ImGui::EndChild(); @@ -806,14 +920,38 @@ namespace TOD float spacing = ImGui::GetStyle().ItemSpacing.x; float columnWidth = (totalWidth - 3 * spacing) / 4.0f; + static std::map wasActiveMap; + static std::map undoPushedMap; + for (int i = 0; i < Count; ++i) { if (i > 0) ImGui::SameLine(); ImGui::PushID(i); + + std::string itemId = std::string(label) + "_" + std::to_string(i); + bool isPreviouslyActive = wasActiveMap[itemId]; + ImGui::SetNextItemWidth(columnWidth); if (ImGui::SliderFloat("##value", &values[i], minValue, maxValue, format)) { changed = true; } + + // Push undo state only once when slider becomes active + bool isNowActive = ImGui::IsItemActive(); + if (isNowActive && !isPreviouslyActive && !undoPushedMap[itemId]) { + if (g_currentWidget) { + EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); + undoPushedMap[itemId] = true; + } + } + + // Reset undo flag when slider is released + if (!isNowActive && undoPushedMap[itemId]) { + undoPushedMap[itemId] = false; + } + + wasActiveMap[itemId] = isNowActive; + ImGui::PopID(); } diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h index e057ab8d43..c9883aba4a 100644 --- a/src/WeatherEditor/WeatherUtils.h +++ b/src/WeatherEditor/WeatherUtils.h @@ -4,6 +4,9 @@ #include #include +// Forward declarations +class Widget; + // Case-insensitive substring search helper bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring); @@ -83,11 +86,14 @@ void EndWidgetSearchBar(); namespace WeatherUtils { + // Set the current widget for undo tracking (should be called at start of widget Draw()) + void SetCurrentWidget(Widget* widget); + // UI helper functions bool DrawSliderInt8(const std::string& label, int& property); - bool DrawColorEdit(const std::string& l, float3& property); + bool DrawColorEdit(const std::string& l, float3& property, Widget* widget = nullptr); bool DrawSliderUint8(const std::string& label, int& property); - bool DrawSliderFloat(const std::string& label, float& property, float min = 0.0f, float max = 50000.0f); + bool DrawSliderFloat(const std::string& label, float& property, float min = 0.0f, float max = 1.0f, Widget* widget = nullptr); // Generic form picker combo box using cached widget EditorIDs for performance // Returns true if selection changed From a8a26706e9acaa00d7540bc1bc96c0b9a768613d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 03:46:52 +0000 Subject: [PATCH 13/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/WeatherEditor/EditorWindow.cpp | 797 ++++++++++---------- src/WeatherEditor/Weather/WeatherWidget.cpp | 646 ++++++++-------- src/WeatherEditor/WeatherUtils.cpp | 46 +- 3 files changed, 753 insertions(+), 736 deletions(-) diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index e0358f2b3f..234e808143 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -2,10 +2,10 @@ #include "Features/WeatherEditor.h" #include "Menu.h" +#include "PaletteWindow.h" #include "State.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" -#include "PaletteWindow.h" #include "imgui_internal.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, enableInheritFromParent, editorUIScale, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) @@ -50,7 +50,7 @@ void DrawIconStar(ImVec2 center, float radius, ImU32 color, bool /*filled*/) float r = (i % 2 == 0) ? radius : radius * 0.38f; points[i] = ImVec2(center.x + cosf(angle) * r, center.y + sinf(angle) * r); } - + for (int i = 0; i < 10; i++) { drawList->AddLine(points[i], points[(i + 1) % 10], color, 1.5f); } @@ -155,49 +155,50 @@ void EditorWindow::ShowObjectsWindow() ImGui::Text("Categories"); ImGui::Spacing(); - // List of categories - const char* categories[] = { "Lighting Template", "Weather", "WorldSpace", "Cell Lighting", "ImageSpace", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect" }; - for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { - // Highlight the selected category - if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { - selectedCategory = categories[i]; // Update selected category - } - } // Right column: Objects - ImGui::TableSetColumnIndex(1); - - // Display current active weather - auto sky = globals::game::sky; - if (sky && sky->currentWeather) { - auto currentWeather = sky->currentWeather; - ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetTheme().StatusPalette.RestartNeeded); - ImGui::Text("Current Active Weather:"); - ImGui::PopStyleColor(); - ImGui::SameLine(); - ImGui::TextColored(Menu::GetSingleton()->GetTheme().Palette.Text, "%s", currentWeather->GetFormEditorID()); - ImGui::SameLine(); - ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); - - // Add button to open the current weather - ImGui::SameLine(); - if (ImGui::SmallButton("Open##CurrentWeather")) { - for (auto& widget : weatherWidgets) { - if (widget->form == currentWeather) { - widget->SetOpen(true); - break; + // List of categories + const char* categories[] = { "Lighting Template", "Weather", "WorldSpace", "Cell Lighting", "ImageSpace", "Volumetric Lighting", "Shader Particle Geometry", "Lens Flare", "Visual Effect" }; + for (int i = 0; i < IM_ARRAYSIZE(categories); ++i) { + // Highlight the selected category + if (ImGui::Selectable(categories[i], selectedCategory == categories[i])) { + selectedCategory = categories[i]; // Update selected category + } + } // Right column: Objects + ImGui::TableSetColumnIndex(1); + + // Display current active weather + auto sky = globals::game::sky; + if (sky && sky->currentWeather) { + auto currentWeather = sky->currentWeather; + ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetTheme().StatusPalette.RestartNeeded); + ImGui::Text("Current Active Weather:"); + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().Palette.Text, "%s", currentWeather->GetFormEditorID()); + ImGui::SameLine(); + ImGui::TextDisabled("(0x%08X)", currentWeather->GetFormID()); + + // Add button to open the current weather + ImGui::SameLine(); + if (ImGui::SmallButton("Open##CurrentWeather")) { + for (auto& widget : weatherWidgets) { + if (widget->form == currentWeather) { + widget->SetOpen(true); + break; + } } } + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); } - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); - } - // Handle Ctrl+F to focus search bar - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { - if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { - ImGui::SetKeyboardFocusHere(); + // Handle Ctrl+F to focus search bar + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { + if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_F, false)) { + ImGui::SetKeyboardFocusHere(); + } } - } ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); + ImGui::InputTextWithHint("##ObjectFilter", "Filter... (Ctrl+F)", filterBuffer, sizeof(filterBuffer)); ImGui::SameLine(); HelpMarker("Type a part of an object name to filter the list.\nCtrl+F: Focus search\nEnter: Open selected"); @@ -228,7 +229,8 @@ void EditorWindow::ShowObjectsWindow() ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Recent:"); ImGui::SameLine(); for (size_t i = 0; i < std::min(size_t(5), recentIt->second.size()); ++i) { - if (i > 0) ImGui::SameLine(); + if (i > 0) + ImGui::SameLine(); if (ImGui::SmallButton(recentIt->second[i].c_str())) { // Find and open widget in current category's collection auto& widgets = selectedCategory == "Weather" ? weatherWidgets : @@ -240,7 +242,7 @@ void EditorWindow::ShowObjectsWindow() selectedCategory == "Lens Flare" ? lensFlareWidgets : selectedCategory == "Visual Effect" ? referenceEffectWidgets : weatherWidgets; - + for (auto& widget : widgets) { if (widget->GetEditorID() == recentIt->second[i]) { widget->SetOpen(true); @@ -275,27 +277,27 @@ void EditorWindow::ShowObjectsWindow() } } - // Display objects based on the selected category - std::vector> emptyWidgets; - const auto& widgets = selectedCategory == "Weather" ? weatherWidgets : - selectedCategory == "WorldSpace" ? worldSpaceWidgets : - selectedCategory == "Cell Lighting" ? emptyWidgets : - selectedCategory == "ImageSpace" ? imageSpaceWidgets : - selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : - selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : - selectedCategory == "Lens Flare" ? lensFlareWidgets : - selectedCategory == "Visual Effect" ? referenceEffectWidgets : - lightingTemplateWidgets; - // Sort widgets based on current sort column - std::vector sortedWidgets; - sortedWidgets.reserve(widgets.size()); - for (const auto& w : widgets) { - sortedWidgets.push_back(w.get()); - } - if (currentSortColumn != SortColumn::None) { - std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { - int comparison = 0; - switch (currentSortColumn) { + // Display objects based on the selected category + std::vector> emptyWidgets; + const auto& widgets = selectedCategory == "Weather" ? weatherWidgets : + selectedCategory == "WorldSpace" ? worldSpaceWidgets : + selectedCategory == "Cell Lighting" ? emptyWidgets : + selectedCategory == "ImageSpace" ? imageSpaceWidgets : + selectedCategory == "Volumetric Lighting" ? volumetricLightingWidgets : + selectedCategory == "Shader Particle Geometry" ? precipitationWidgets : + selectedCategory == "Lens Flare" ? lensFlareWidgets : + selectedCategory == "Visual Effect" ? referenceEffectWidgets : + lightingTemplateWidgets; + // Sort widgets based on current sort column + std::vector sortedWidgets; + sortedWidgets.reserve(widgets.size()); + for (const auto& w : widgets) { + sortedWidgets.push_back(w.get()); + } + if (currentSortColumn != SortColumn::None) { + std::sort(sortedWidgets.begin(), sortedWidgets.end(), [this](Widget* a, Widget* b) { + int comparison = 0; + switch (currentSortColumn) { case SortColumn::EditorID: comparison = _stricmp(a->GetEditorID().c_str(), b->GetEditorID().c_str()); break; @@ -316,306 +318,306 @@ void EditorWindow::ShowObjectsWindow() } default: break; - } - return sortAscending ? (comparison < 0) : (comparison > 0); - }); - } + } + return sortAscending ? (comparison < 0) : (comparison > 0); + }); + } - // Special handling for Cell Lighting category - if (selectedCategory == "Cell Lighting") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto cell = player->parentCell; - bool isInterior = cell->IsInteriorCell(); - - if (isInterior) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - - // No favorite star for cell lighting (it's always the current cell) - ImGui::Dummy(ImVec2(24, 24)); - - ImGui::TableNextColumn(); - - // Display current cell name - const char* cellName = cell->GetName(); - std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; - std::string label = std::format("[CURRENT CELL] {}", displayName); - - bool isOpen = currentCellLightingWidget && currentCellLightingWidget->IsOpen(); - if (ImGui::Selectable(label.c_str(), isOpen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - // Open or reuse the cell lighting widget + // Special handling for Cell Lighting category + if (selectedCategory == "Cell Lighting") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto cell = player->parentCell; + bool isInterior = cell->IsInteriorCell(); + + if (isInterior) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + + // No favorite star for cell lighting (it's always the current cell) + ImGui::Dummy(ImVec2(24, 24)); + + ImGui::TableNextColumn(); + + // Display current cell name + const char* cellName = cell->GetName(); + std::string displayName = cellName && cellName[0] ? cellName : "[Unnamed Cell]"; + std::string label = std::format("[CURRENT CELL] {}", displayName); + + bool isOpen = currentCellLightingWidget && currentCellLightingWidget->IsOpen(); + if (ImGui::Selectable(label.c_str(), isOpen, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + // Open or reuse the cell lighting widget + if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { + currentCellLightingWidget->SetOpen(true); + } else { + currentCellLightingWidget = std::make_unique(cell); + currentCellLightingWidget->CacheFormData(); + currentCellLightingWidget->Load(); + currentCellLightingWidget->SetOpen(true); + } + } + } + + // Highlight current cell + auto highlightColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor; + highlightColor.w = 0.3f; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(highlightColor)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(highlightColor)); + + // Enter key to open + if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { currentCellLightingWidget->SetOpen(true); - } else { - currentCellLightingWidget = std::make_unique(cell); - currentCellLightingWidget->CacheFormData(); - currentCellLightingWidget->Load(); - currentCellLightingWidget->SetOpen(true); } } + + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", cell->GetFormID()); + + // File column + ImGui::TableNextColumn(); + auto file = cell->GetFile(0); + if (file) { + ImGui::Text("%s", file->fileName); + } + + // Status column + ImGui::TableNextColumn(); + ImGui::Text("Interior Cell"); + } else { + // Show message that cell lighting is only for interior cells + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Warning, "Cell Lighting is only available for interior cells."); + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable, "You are currently in an exterior cell."); } - - // Highlight current cell + } else { + // No player or cell + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(1); + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Error, "Player cell not available."); + } + } + + // Get current cell's lighting template for prioritization + RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; + if (selectedCategory == "Lighting Template") { + auto player = RE::PlayerCharacter::GetSingleton(); + if (player && player->parentCell) { + auto& cellData = player->parentCell->GetRuntimeData(); + currentCellLightingTemplate = cellData.lightingTemplate; + } + } + + // Filtered display of widgets - show current cell's lighting template first + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + for (int i = 0; i < sortedWidgets.size(); ++i) { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) + continue; + + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + continue; + + // Apply quick filters + if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) + continue; + if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) + continue; + + auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); + auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); + ImGui::TableNextRow(); + + // Highlight current cell's lighting template auto highlightColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor; highlightColor.w = 0.3f; ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(highlightColor)); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(highlightColor)); - + + ImGui::TableSetColumnIndex(0); + + // Favorite star + if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + ToggleFavorite(sortedWidgets[i]->GetEditorID()); + } + + ImGui::TableNextColumn(); + + // Editor ID column with [CURRENT] prefix + bool isSelected = sortedWidgets[i]->IsOpen(); + if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } + } + // Enter key to open - if (isOpen && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - if (currentCellLightingWidget && currentCellLightingWidget->cell == cell) { - currentCellLightingWidget->SetOpen(true); + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } + + // Context menu + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; + + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; + Save(); + } + } + + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(sortedWidgets[i]->GetEditorID()); + Save(); } + + ImGui::EndPopup(); } - + // Form ID column ImGui::TableNextColumn(); - ImGui::Text("0x%08X", cell->GetFormID()); - + ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); + // File column ImGui::TableNextColumn(); - auto file = cell->GetFile(0); - if (file) { - ImGui::Text("%s", file->fileName); - } - + ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); + // Status column ImGui::TableNextColumn(); - ImGui::Text("Interior Cell"); - } else { - // Show message that cell lighting is only for interior cells - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Warning, "Cell Lighting is only available for interior cells."); - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable, "You are currently in an exterior cell."); - } - } else { - // No player or cell - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(1); - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Error, "Player cell not available."); - } - } - - // Get current cell's lighting template for prioritization - RE::BGSLightingTemplate* currentCellLightingTemplate = nullptr; - if (selectedCategory == "Lighting Template") { - auto player = RE::PlayerCharacter::GetSingleton(); - if (player && player->parentCell) { - auto& cellData = player->parentCell->GetRuntimeData(); - currentCellLightingTemplate = cellData.lightingTemplate; - } - } - - // Filtered display of widgets - show current cell's lighting template first - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - for (int i = 0; i < sortedWidgets.size(); ++i) { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (!ltWidget || ltWidget->lightingTemplate != currentCellLightingTemplate) - continue; - - if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) - continue; - - // Apply quick filters - if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) - continue; - if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) - continue; - - auto editorLabel = std::format("[CURRENT] {}", sortedWidgets[i]->GetEditorID()); - auto markedRecord = settings.markedRecords.find(sortedWidgets[i]->GetEditorID()); - ImGui::TableNextRow(); - - // Highlight current cell's lighting template - auto highlightColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor; - highlightColor.w = 0.3f; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(highlightColor)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(highlightColor)); - - ImGui::TableSetColumnIndex(0); - - // Favorite star - if (IconButton("##fav_current", IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { - ToggleFavorite(sortedWidgets[i]->GetEditorID()); - } - - ImGui::TableNextColumn(); - - // Editor ID column with [CURRENT] prefix - bool isSelected = sortedWidgets[i]->IsOpen(); - if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - } - - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - - // Context menu - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { - auto& markedRecords = settings.markedRecords; - - for (auto& recordMarker : settings.recordMarkers) { - if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[sortedWidgets[i]->GetEditorID()] = recordMarker.first; - Save(); + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text("%s", markedRecord->second.c_str()); } } + } - if (ImGui::MenuItem("Remove")) { - markedRecords.erase(sortedWidgets[i]->GetEditorID()); - Save(); + // Filtered display of widgets - regular list + for (int i = 0; i < sortedWidgets.size(); ++i) { + // Skip current cell's lighting template if already shown + if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { + auto* ltWidget = dynamic_cast(sortedWidgets[i]); + if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) + continue; } - ImGui::EndPopup(); - } - - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); + if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) + continue; - // File column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); + // Apply quick filters + if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) + continue; + if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) + continue; - // Status column - ImGui::TableNextColumn(); - if (markedRecord != settings.markedRecords.end()) { - ImGui::Text("%s", markedRecord->second.c_str()); - } - } - } + auto editorLabel = sortedWidgets[i]->GetEditorID(); + auto markedRecord = settings.markedRecords.find(editorLabel); + ImGui::TableNextRow(); - // Filtered display of widgets - regular list - for (int i = 0; i < sortedWidgets.size(); ++i) { - // Skip current cell's lighting template if already shown - if (currentCellLightingTemplate && selectedCategory == "Lighting Template") { - auto* ltWidget = dynamic_cast(sortedWidgets[i]); - if (ltWidget && ltWidget->lightingTemplate == currentCellLightingTemplate) - continue; - } + // Set background colour + if (markedRecord != settings.markedRecords.end()) { + auto& color = settings.recordMarkers[markedRecord->second]; + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); + } - if (!ContainsStringIgnoreCase(sortedWidgets[i]->GetEditorID(), filterBuffer)) - continue; + ImGui::TableSetColumnIndex(0); - // Apply quick filters - if (showOnlyFavorites && !IsFavorite(sortedWidgets[i]->GetEditorID())) - continue; - if (showOnlyFlagged && settings.markedRecords.find(sortedWidgets[i]->GetEditorID()) == settings.markedRecords.end()) - continue; + // Favorite star + if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { + ToggleFavorite(sortedWidgets[i]->GetEditorID()); + } - auto editorLabel = sortedWidgets[i]->GetEditorID(); - auto markedRecord = settings.markedRecords.find(editorLabel); - ImGui::TableNextRow(); + ImGui::TableNextColumn(); - // Set background colour - if (markedRecord != settings.markedRecords.end()) { - auto& color = settings.recordMarkers[markedRecord->second]; - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(color)); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(color)); - } + // Editor ID column + bool isSelected = sortedWidgets[i]->IsOpen(); + if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { + if (ImGui::IsMouseDoubleClicked(0)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); + } + } - ImGui::TableSetColumnIndex(0); + // Show ImageSpace and VolumetricLighting info for weather widgets + if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { + auto* weatherWidget = dynamic_cast(sortedWidgets[i]); + if (weatherWidget && weatherWidget->weather) { + ImGui::BeginTooltip(); + + // ImageSpace info + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "ImageSpace:"); + for (int tod = 0; tod < 4; tod++) { + auto imgSpace = weatherWidget->weather->imageSpaces[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + imgSpace ? imgSpace->GetFormEditorID() : "None"); + } - // Favorite star - if (IconButton(std::format("##fav_{}", i).c_str(), IsFavorite(sortedWidgets[i]->GetEditorID()), "star")) { - ToggleFavorite(sortedWidgets[i]->GetEditorID()); - } + ImGui::Spacing(); - ImGui::TableNextColumn(); + // VolumetricLighting info + ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "Volumetric Lighting:"); + for (int tod = 0; tod < 4; tod++) { + auto volLight = weatherWidget->weather->volumetricLighting[tod]; + ImGui::Text(" %s: %s", + TOD::GetPeriodName(tod), + volLight ? volLight->GetFormEditorID() : "None"); + } - // Editor ID column - bool isSelected = sortedWidgets[i]->IsOpen(); - if (ImGui::Selectable(editorLabel.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowDoubleClick)) { - if (ImGui::IsMouseDoubleClicked(0)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - } - - // Show ImageSpace and VolumetricLighting info for weather widgets - if (selectedCategory == "Weather" && ImGui::IsItemHovered()) { - auto* weatherWidget = dynamic_cast(sortedWidgets[i]); - if (weatherWidget && weatherWidget->weather) { - ImGui::BeginTooltip(); - - // ImageSpace info - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "ImageSpace:"); - for (int tod = 0; tod < 4; tod++) { - auto imgSpace = weatherWidget->weather->imageSpaces[tod]; - ImGui::Text(" %s: %s", - TOD::GetPeriodName(tod), - imgSpace ? imgSpace->GetFormEditorID() : "None"); + ImGui::EndTooltip(); + } } - - ImGui::Spacing(); - - // VolumetricLighting info - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "Volumetric Lighting:"); - for (int tod = 0; tod < 4; tod++) { - auto volLight = weatherWidget->weather->volumetricLighting[tod]; - ImGui::Text(" %s: %s", - TOD::GetPeriodName(tod), - volLight ? volLight->GetFormEditorID() : "None"); + + // Enter key to open + if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { + sortedWidgets[i]->SetOpen(true); + AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); } - - ImGui::EndTooltip(); - } - } - - // Enter key to open - if (isSelected && ImGui::IsKeyPressed(ImGuiKey_Enter)) { - sortedWidgets[i]->SetOpen(true); - AddToRecent(sortedWidgets[i]->GetEditorID(), selectedCategory); - } - // Opens a context menu on right click to mark records by color - if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { - auto& markedRecords = settings.markedRecords; + // Opens a context menu on right click to mark records by color + if (ImGui::BeginPopupContextItem(std::format("widget_context_menu##{}", sortedWidgets[i]->GetFormID()).c_str(), ImGuiPopupFlags_MouseButtonRight)) { + auto& markedRecords = settings.markedRecords; - for (auto& recordMarker : settings.recordMarkers) { - if (ImGui::MenuItem(recordMarker.first.c_str())) { - settings.markedRecords[editorLabel] = recordMarker.first; - Save(); - } - } + for (auto& recordMarker : settings.recordMarkers) { + if (ImGui::MenuItem(recordMarker.first.c_str())) { + settings.markedRecords[editorLabel] = recordMarker.first; + Save(); + } + } - if (ImGui::MenuItem("Remove")) { - markedRecords.erase(editorLabel); - Save(); - } + if (ImGui::MenuItem("Remove")) { + markedRecords.erase(editorLabel); + Save(); + } - ImGui::EndPopup(); - } + ImGui::EndPopup(); + } - // Form ID column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); + // Form ID column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFormID().c_str()); - // File column - ImGui::TableNextColumn(); - ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); + // File column + ImGui::TableNextColumn(); + ImGui::Text(sortedWidgets[i]->GetFilename().c_str()); - // Status column - ImGui::TableNextColumn(); + // Status column + ImGui::TableNextColumn(); - // Re-check if the record exists after potential removal - markedRecord = settings.markedRecords.find(editorLabel); - if (markedRecord != settings.markedRecords.end()) { - ImGui::Text("%s", markedRecord->second.c_str()); - } - } + // Re-check if the record exists after potential removal + markedRecord = settings.markedRecords.find(editorLabel); + if (markedRecord != settings.markedRecords.end()) { + ImGui::Text("%s", markedRecord->second.c_str()); + } + } - ImGui::EndTable(); // End DetailsTable - } // End if BeginTable("DetailsTable") + ImGui::EndTable(); // End DetailsTable + } // End if BeginTable("DetailsTable") ImGui::EndTable(); // End ObjectTable } // End if BeginTable("ObjectTable") @@ -887,7 +889,7 @@ void EditorWindow::RenderUI() settingsSelectedCategory = "Flags"; } ImGui::Separator(); - + // Current cell lighting auto player = RE::PlayerCharacter::GetSingleton(); if (player && player->parentCell && player->parentCell->IsInteriorCell()) { @@ -898,7 +900,7 @@ void EditorWindow::RenderUI() currentCellLightingWidget->SetOpen(true); found = true; } - + if (!found) { // Create new widget for current cell currentCellLightingWidget = std::make_unique(player->parentCell); @@ -915,9 +917,9 @@ void EditorWindow::RenderUI() ImGui::SetTooltip("Only available in interior cells"); } } - + ImGui::Separator(); - + if (ImGui::Checkbox("Auto-Apply Changes", &settings.autoApplyChanges)) { Save(); } @@ -942,7 +944,7 @@ void EditorWindow::RenderUI() if (ImGui::MenuItem("Palette", nullptr, PaletteWindow::GetSingleton()->open)) { PaletteWindow::GetSingleton()->open = !PaletteWindow::GetSingleton()->open; } - + ImGui::Separator(); ImGui::Text("Open Widgets:"); ImGui::Separator(); @@ -1013,7 +1015,7 @@ void EditorWindow::RenderUI() ImGui::BulletText("ImageSpaces: %d", (int)imageSpaceWidgets.size()); ImGui::Separator(); ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.CurrentHotkey, "Favorites: %d", (int)settings.favoriteWidgets.size()); - + // Count total recent widgets across all categories int totalRecent = 0; for (const auto& [category, widgets] : settings.recentWidgets) { @@ -1056,56 +1058,56 @@ void EditorWindow::RenderUI() } } - ImGui::PopStyleColor(2); - ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(isPaused ? "Resume Time" : "Pause Time"); + } } - } - // Undo button - if (menu && menu->uiIcons.undo.texture) { - bool canUndo = CanUndo(); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); - if (!canUndo) { - auto transparentColor = ImVec4(0, 0, 0, 0); - ImGui::PushStyleColor(ImGuiCol_Button, transparentColor); - auto disabledColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable; - disabledColor.w = 0.25f; - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, disabledColor); - auto disabledTextColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable; - disabledTextColor.w = 0.5f; - ImGui::PushStyleColor(ImGuiCol_Text, disabledTextColor); - } else { - auto transparentColor = ImVec4(0, 0, 0, 0); - ImGui::PushStyleColor(ImGuiCol_Button, transparentColor); - auto hoverColor = Menu::GetSingleton()->GetSettings().Theme.Palette.Text; - hoverColor.w = 0.25f; - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoverColor); - ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetSettings().Theme.Palette.Text); - } + // Undo button + if (menu && menu->uiIcons.undo.texture) { + bool canUndo = CanUndo(); - const float menuBarHeight = ImGui::GetFrameHeight(); - const float buttonDim = menuBarHeight * 0.85f; - const ImVec2 buttonSize(buttonDim, buttonDim); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + if (!canUndo) { + auto transparentColor = ImVec4(0, 0, 0, 0); + ImGui::PushStyleColor(ImGuiCol_Button, transparentColor); + auto disabledColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable; + disabledColor.w = 0.25f; + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, disabledColor); + auto disabledTextColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable; + disabledTextColor.w = 0.5f; + ImGui::PushStyleColor(ImGuiCol_Text, disabledTextColor); + } else { + auto transparentColor = ImVec4(0, 0, 0, 0); + ImGui::PushStyleColor(ImGuiCol_Button, transparentColor); + auto hoverColor = Menu::GetSingleton()->GetSettings().Theme.Palette.Text; + hoverColor.w = 0.25f; + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoverColor); + ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetSettings().Theme.Palette.Text); + } - if (ImGui::ImageButton("##GlobalUndo", menu->uiIcons.undo.texture, buttonSize) && canUndo) { - PerformUndo(); - } + const float menuBarHeight = ImGui::GetFrameHeight(); + const float buttonDim = menuBarHeight * 0.85f; + const ImVec2 buttonSize(buttonDim, buttonDim); - ImGui::PopStyleColor(3); - ImGui::PopStyleVar(); + if (ImGui::ImageButton("##GlobalUndo", menu->uiIcons.undo.texture, buttonSize) && canUndo) { + PerformUndo(); + } - if (ImGui::IsItemHovered()) { - if (canUndo) { - ImGui::SetTooltip("Undo (Ctrl+Z) - %d states", (int)undoStack.size()); - } else { - ImGui::SetTooltip("Undo (Ctrl+Z) - No changes to undo"); + ImGui::PopStyleColor(3); + ImGui::PopStyleVar(); + + if (ImGui::IsItemHovered()) { + if (canUndo) { + ImGui::SetTooltip("Undo (Ctrl+Z) - %d states", (int)undoStack.size()); + } else { + ImGui::SetTooltip("Undo (Ctrl+Z) - No changes to undo"); + } } - } - } // Weather lock indicator + } // Weather lock indicator if (weatherLockActive && lockedWeather) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, Menu::GetSingleton()->GetSettings().Theme.StatusPalette.SuccessColor); @@ -1121,28 +1123,29 @@ void EditorWindow::RenderUI() ImGui::Text(" [TIME PAUSED]"); ImGui::PopStyleColor(); } - - // Close button on the right side - float menuBarHeight = ImGui::GetFrameHeight(); - float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar - ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); - auto errorColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Error; - auto errorHoverColor = errorColor; - errorHoverColor.x = std::min(1.0f, errorColor.x * 1.2f); - errorHoverColor.y = std::min(1.0f, errorColor.y * 0.75f); - auto errorActiveColor = errorColor; - errorActiveColor.x = std::max(0.0f, errorColor.x * 0.875f); - errorActiveColor.y = std::max(0.0f, errorColor.y * 0.25f); - ImGui::PushStyleColor(ImGuiCol_Button, errorColor); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, errorHoverColor); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, errorActiveColor); - if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { - open = false; - } - ImGui::PopStyleColor(3); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Close Weather Editor (Esc)"); - } ImGui::EndMainMenuBar(); + + // Close button on the right side + float menuBarHeight = ImGui::GetFrameHeight(); + float closeButtonSize = menuBarHeight * 0.9f; // 10% smaller than menu bar + ImGui::SameLine(ImGui::GetWindowWidth() - closeButtonSize - 10.0f); + auto errorColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Error; + auto errorHoverColor = errorColor; + errorHoverColor.x = std::min(1.0f, errorColor.x * 1.2f); + errorHoverColor.y = std::min(1.0f, errorColor.y * 0.75f); + auto errorActiveColor = errorColor; + errorActiveColor.x = std::max(0.0f, errorColor.x * 0.875f); + errorActiveColor.y = std::max(0.0f, errorColor.y * 0.25f); + ImGui::PushStyleColor(ImGuiCol_Button, errorColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, errorHoverColor); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, errorActiveColor); + if (ImGui::Button("X", ImVec2(closeButtonSize, closeButtonSize))) { + open = false; + } + ImGui::PopStyleColor(3); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Close Weather Editor (Esc)"); + } + ImGui::EndMainMenuBar(); } auto width = ImGui::GetIO().DisplaySize.x; @@ -1164,7 +1167,7 @@ void EditorWindow::RenderUI() } ShowWidgetWindow(); - + // Show palette window PaletteWindow::GetSingleton()->Draw(); @@ -1173,7 +1176,7 @@ void EditorWindow::RenderUI() // Pop the alpha style var ImGui::PopStyleVar(); - + // Restore previous font scale io.FontGlobalScale = previousScale; } @@ -1408,26 +1411,26 @@ void EditorWindow::ShowSettingsWindow() ImGui::Checkbox("Use text buttons instead of icons", &settings.useTextButtons); AddTooltip("Display action buttons as text labels instead of icons"); - + ImGui::Checkbox("Enable 'Inherit From Parent' feature", &settings.enableInheritFromParent); AddTooltip("Show checkboxes to copy settings from parent weather (editor-only feature)"); - + ImGui::Separator(); ImGui::TextUnformatted("UI Scale"); ImGui::Spacing(); - + if (ImGui::SliderFloat("Editor UI Scale", &settings.editorUIScale, 0.5f, 2.0f, "%.2f")) { Save(); } AddTooltip("Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)"); - + if (ImGui::Button("Reset to 1.0")) { settings.editorUIScale = 1.0f; Save(); } ImGui::SameLine(); AddTooltip("Reset UI scale to default (100%)"); - + ImGui::Separator(); ImGui::TextUnformatted("Session & History"); ImGui::Spacing(); @@ -1447,7 +1450,7 @@ void EditorWindow::ShowSettingsWindow() settings.favoriteWidgets.clear(); Save(); } - + } else if (settingsSelectedCategory == "Flags") { if (ImGui::BeginTable("FlagsTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("Label", ImGuiTableColumnFlags_WidthStretch); @@ -1686,7 +1689,7 @@ void EditorWindow::DisableVanityCamera() } } - void EditorWindow::RestoreVanityCamera() +void EditorWindow::RestoreVanityCamera() { if (!vanityCameraDisabled) return; @@ -1819,7 +1822,7 @@ void EditorWindow::RenderNotifications() void EditorWindow::AddToRecent(const std::string& widgetId, const std::string& category) { auto& categoryRecent = settings.recentWidgets[category]; - + // Remove if already exists auto it = std::find(categoryRecent.begin(), categoryRecent.end(), widgetId); if (it != categoryRecent.end()) { diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 477db683e0..92503711da 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -73,10 +73,9 @@ void WeatherWidget::DrawWidget() ImGui::SetNextWindowFocus(); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.16f, 0.16f, 0.16f, 1.0f)); - if (ImGui::Begin("##SearchDropdown", nullptr, - ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { - + if (ImGui::Begin("##SearchDropdown", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings)) { for (size_t i = 0; i < std::min(size_t(5), searchResults.size()); ++i) { const auto& result = searchResults[i]; std::string label = std::format("{} ({})", result.displayName, result.tabName); @@ -108,113 +107,114 @@ void WeatherWidget::DrawWidget() auto& widgets = editorWindow->weatherWidgets; // Sets the parent widget if settings have been loaded. - if (settings.parent != "None") { - parent = GetParent(); - if (parent == nullptr) - settings.parent = "None"; - } - - if (editorWindow->settings.enableInheritFromParent) { - if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { - // Option for "None" - if (ImGui::Selectable("None", parent == nullptr)) { - parent = nullptr; + if (settings.parent != "None") { + parent = GetParent(); + if (parent == nullptr) settings.parent = "None"; - } + } - for (int i = 0; i < widgets.size(); i++) { - auto& widget = widgets[i]; + if (editorWindow->settings.enableInheritFromParent) { + if (ImGui::BeginCombo("Parent", settings.parent.c_str())) { + // Option for "None" + if (ImGui::Selectable("None", parent == nullptr)) { + parent = nullptr; + settings.parent = "None"; + } - // Skip self-selection - if (widget.get() == this) - continue; + for (int i = 0; i < widgets.size(); i++) { + auto& widget = widgets[i]; - // Option for each widget - if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget.get())) { - parent = (WeatherWidget*)widget.get(); - settings.parent = widget->GetEditorID(); - } + // Skip self-selection + if (widget.get() == this) + continue; - // Set default focus to the current parent - if (parent == widget.get()) { - ImGui::SetItemDefaultFocus(); + // Option for each widget + if (ImGui::Selectable(widget->GetEditorID().c_str(), parent == widget.get())) { + parent = (WeatherWidget*)widget.get(); + settings.parent = widget->GetEditorID(); + } + + // Set default focus to the current parent + if (parent == widget.get()) { + ImGui::SetItemDefaultFocus(); + } } + ImGui::EndCombo(); } - ImGui::EndCombo(); - } - ImGui::SameLine(); - ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered()) { - ImGui::BeginTooltip(); - ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); - ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); - ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "Note: This is NOT the same as cell lighting template inheritance."); - ImGui::EndTooltip(); - } - - if (parent) { ImGui::SameLine(); - if (ImGui::Button("Inherit All")) { - InheritAllFromParent(); - } + ImGui::TextDisabled("(?)"); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Copy all parameter values from parent weather"); + ImGui::BeginTooltip(); + ImGui::TextUnformatted("Editor-only feature: Set a parent weather to copy settings from."); + ImGui::TextUnformatted("Use 'Inherit From Parent' checkboxes to copy specific values."); + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.0f, 1.0f), "Note: This is NOT the same as cell lighting template inheritance."); + ImGui::EndTooltip(); } - - if (!parent->IsOpen()) { + + if (parent) { ImGui::SameLine(); - if (ImGui::Button("Open")) - parent->SetOpen(true); + if (ImGui::Button("Inherit All")) { + InheritAllFromParent(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Copy all parameter values from parent weather"); + } + + if (!parent->IsOpen()) { + ImGui::SameLine(); + if (ImGui::Button("Open")) + parent->SetOpen(true); + } } } - } -} // Tab bar for organizing settings - if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { - // Use activeTabOverride to auto-navigate to specific tab - ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags dalcFlags = (activeTabOverride == "Lighting (DALC)") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags atmosphereFlags = (activeTabOverride == "Atmosphere Colors") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; - ImGuiTabItemFlags recordsFlags = (activeTabOverride == "Records") ? ImGuiTabItemFlags_SetSelected : 0; - if (!activeTabOverride.empty()) { - activeTabOverride = ""; // Clear after use - } + } // Tab bar for organizing settings + if (ImGui::BeginTabBar("WeatherSettingsTabs", ImGuiTabBarFlags_None)) { + // Use activeTabOverride to auto-navigate to specific tab + ImGuiTabItemFlags basicFlags = (activeTabOverride == "Basic") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags dalcFlags = (activeTabOverride == "Lighting (DALC)") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags atmosphereFlags = (activeTabOverride == "Atmosphere Colors") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags cloudsFlags = (activeTabOverride == "Clouds") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags fogFlags = (activeTabOverride == "Fog") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags featuresFlags = (activeTabOverride == "Features") ? ImGuiTabItemFlags_SetSelected : 0; + ImGuiTabItemFlags recordsFlags = (activeTabOverride == "Records") ? ImGuiTabItemFlags_SetSelected : 0; + if (!activeTabOverride.empty()) { + activeTabOverride = ""; // Clear after use + } - if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { - DrawProperties("Sun", { { "Sun Damage", INT8_SLIDER } }); - DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); - DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); - DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, + if (ImGui::BeginTabItem("Basic", nullptr, basicFlags)) { + DrawProperties("Sun", { { "Sun Damage", INT8_SLIDER } }); + DrawProperties("Wind", { { "Wind Speed", UINT8_SLIDER }, { "Wind Direction", INT8_SLIDER }, { "Wind Direction Range", INT8_SLIDER } }); + DrawProperties("Precipitation", { { "Precipitation Begin Fade In", INT8_SLIDER }, { "Precipitation Begin Fade Out", INT8_SLIDER } }); + DrawProperties("Lightning", { { "Thunder Lightning Begin Fade In", INT8_SLIDER }, { "Thunder Lightning End Fade Out", INT8_SLIDER }, { "Thunder Lightning Frequency", INT8_SLIDER }, { "Lightning Color", COLOR3_PICKER } }); - DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); - DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); - ImGui::EndTabItem(); - } if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { - DrawDALCSettings(); - ImGui::EndTabItem(); - } + DrawProperties("Visual Effects", { { "Visual Effect Begin", INT8_SLIDER }, { "Visual Effect End", INT8_SLIDER } }); + DrawProperties("Weather Transition", { { "Trans Delta", INT8_SLIDER } }); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Lighting (DALC)", nullptr, dalcFlags)) { + DrawDALCSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Atmosphere Colors", nullptr, atmosphereFlags)) { - DrawWeatherColorSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Atmosphere Colors", nullptr, atmosphereFlags)) { + DrawWeatherColorSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Clouds", nullptr, cloudsFlags)) { - DrawCloudSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Clouds", nullptr, cloudsFlags)) { + DrawCloudSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Fog", nullptr, fogFlags)) { - DrawFogSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Fog", nullptr, fogFlags)) { + DrawFogSettings(); + ImGui::EndTabItem(); + } - if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { - DrawFeatureSettings(); - ImGui::EndTabItem(); - } + if (ImGui::BeginTabItem("Features", nullptr, featuresFlags)) { + DrawFeatureSettings(); + ImGui::EndTabItem(); + } if (ImGui::BeginTabItem("Records", nullptr, recordsFlags)) { ImGui::Spacing(); @@ -224,36 +224,37 @@ void WeatherWidget::DrawWidget() ImGui::Spacing(); auto* editorWindow = EditorWindow::GetSingleton(); - bool recordChanged = false; - bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); - WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; - - // ImageSpace Records (per time of day) - if (ImGui::CollapsingHeader("ImageSpace", ImGuiTreeNodeFlags_DefaultOpen)) { - for (int i = 0; i < ColorTimes::kTotal; i++) { - ImGui::PushID(i); - std::string label = ColorTimeLabel(i); - std::string inheritKey = "ImageSpace_" + std::to_string(i); - - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags[inheritKey]; - if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i]; - recordChanged = true; + bool recordChanged = false; + bool hasParent = editorWindow->settings.enableInheritFromParent && HasParent(); + WeatherWidget* parentWidget = hasParent ? GetParent() : nullptr; + + // ImageSpace Records (per time of day) + if (ImGui::CollapsingHeader("ImageSpace", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "ImageSpace_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->imageSpaces[i] = parentWidget->weather->imageSpaces[i]; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("%s:", label.c_str()); - ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); + if (WeatherUtils::DrawFormPickerCached("##ImageSpace", weather->imageSpaces[i], editorWindow->imageSpaceWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->imageSpaces[i]) { ImGui::SameLine(); if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { @@ -268,38 +269,39 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this ImageSpace for editing"); } } - + ImGui::PopID(); } ImGui::Spacing(); } - // Volumetric Lighting Records (per time of day) - if (ImGui::CollapsingHeader("Volumetric Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { - for (int i = 0; i < ColorTimes::kTotal; i++) { - ImGui::PushID(100 + i); - std::string label = ColorTimeLabel(i); - std::string inheritKey = "VolumetricLighting_" + std::to_string(i); - - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags[inheritKey]; - if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i]; - recordChanged = true; + // Volumetric Lighting Records (per time of day) + if (ImGui::CollapsingHeader("Volumetric Lighting", ImGuiTreeNodeFlags_DefaultOpen)) { + for (int i = 0; i < ColorTimes::kTotal; i++) { + ImGui::PushID(100 + i); + std::string label = ColorTimeLabel(i); + std::string inheritKey = "VolumetricLighting_" + std::to_string(i); + + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags[inheritKey]; + if (ImGui::Checkbox(("##inherit_" + inheritKey).c_str(), &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->volumetricLighting[i] = parentWidget->weather->volumetricLighting[i]; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("%s:", label.c_str()); - ImGui::SameLine(hasParent ? 120.0f : 100.0f); if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("%s:", label.c_str()); + ImGui::SameLine(hasParent ? 120.0f : 100.0f); + if (WeatherUtils::DrawFormPickerCached("##VolumetricLighting", weather->volumetricLighting[i], editorWindow->volumetricLightingWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->volumetricLighting[i]) { ImGui::SameLine(); if (ImGui::SmallButton(std::format("Open##{}", i).c_str())) { @@ -314,33 +316,34 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Volumetric Lighting for editing"); } } - + ImGui::PopID(); } ImGui::Spacing(); } - // Precipitation Data - if (ImGui::CollapsingHeader("Precipitation", ImGuiTreeNodeFlags_DefaultOpen)) { - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags["Precipitation"]; - if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->precipitationData = parentWidget->weather->precipitationData; - recordChanged = true; + // Precipitation Data + if (ImGui::CollapsingHeader("Precipitation", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["Precipitation"]; + if (ImGui::Checkbox("##inherit_Precipitation", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->precipitationData = parentWidget->weather->precipitationData; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("Particle Shader:"); - ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("Particle Shader:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); + if (WeatherUtils::DrawFormPickerCached("##Precipitation", weather->precipitationData, editorWindow->precipitationWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->precipitationData) { ImGui::SameLine(); if (ImGui::SmallButton("Open##Precip")) { @@ -355,31 +358,32 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Precipitation for editing"); } } - + ImGui::Spacing(); } - // Visual Effect (Reference Effect) - if (ImGui::CollapsingHeader("Visual Effect", ImGuiTreeNodeFlags_DefaultOpen)) { - // Inherit checkbox - if (hasParent) { - bool& inheritFlag = settings.inheritFlags["ReferenceEffect"]; - if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) { - if (inheritFlag && parentWidget) { - weather->referenceEffect = parentWidget->weather->referenceEffect; - recordChanged = true; + // Visual Effect (Reference Effect) + if (ImGui::CollapsingHeader("Visual Effect", ImGuiTreeNodeFlags_DefaultOpen)) { + // Inherit checkbox + if (hasParent) { + bool& inheritFlag = settings.inheritFlags["ReferenceEffect"]; + if (ImGui::Checkbox("##inherit_ReferenceEffect", &inheritFlag)) { + if (inheritFlag && parentWidget) { + weather->referenceEffect = parentWidget->weather->referenceEffect; + recordChanged = true; + } } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); + } + ImGui::SameLine(); } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip(inheritFlag ? "Inheriting from parent" : "Inherit from parent"); - } - ImGui::SameLine(); - } - - ImGui::Text("Reference Effect:"); - ImGui::SameLine(hasParent ? 170.0f : 150.0f); if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true)) { - recordChanged = true; - } // Add "Open" button + + ImGui::Text("Reference Effect:"); + ImGui::SameLine(hasParent ? 170.0f : 150.0f); + if (WeatherUtils::DrawFormPickerCached("##ReferenceEffect", weather->referenceEffect, editorWindow->referenceEffectWidgets, false, true)) { + recordChanged = true; + } // Add "Open" button if (weather->referenceEffect) { ImGui::SameLine(); if (ImGui::SmallButton("Open##RefEffect")) { @@ -394,24 +398,24 @@ void WeatherWidget::DrawWidget() ImGui::SetTooltip("Open this Visual Effect for editing"); } } - - - ImGui::Spacing(); - } - if (recordChanged) { - } + ImGui::Spacing(); + } - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); + if (recordChanged) { + } + + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } ImGui::End(); } void WeatherWidget::LoadSettings() { - bool hadErrors = false; if (!js.empty()) { + bool hadErrors = false; + if (!js.empty()) { try { // Attempt to load settings from JSON settings = js; @@ -467,7 +471,7 @@ void WeatherWidget::SaveSettings() try { js = settings; - + if (js.is_null()) { logger::error("Weather {}: Serialization produced null JSON!", GetEditorID()); } else if (!js.contains("weatherProperties")) { @@ -578,7 +582,7 @@ void WeatherWidget::SetWeatherValues() weather->cloudLayerSpeedX[i] = (int8_t)settingsCloud.cloudLayerSpeedX; weather->cloudLayerSpeedY[i] = (int8_t)settingsCloud.cloudLayerSpeedY; - + if (!settingsCloud.enabled) { disabledBits |= (1 << i); } @@ -871,23 +875,23 @@ void WeatherWidget::DrawWeatherColorSettings() // Organized display order: group related sky/fog/lighting properties static const int displayOrder[] = { - 0, // Sky Upper - 7, // Sky Lower - 8, // Horizon - 1, // Fog Near - 12, // Fog Far - 3, // Ambient - 4, // Sunlight - 15, // Sun Glare - 5, // Sun - 6, // Stars - 16, // Moon Glare - 9, // Effect Lighting - 10, // Cloud LOD Diffuse - 11, // Cloud LOD Ambient - 13, // Sky Statics - 14, // Water Multiplier - 2, // Unknown + 0, // Sky Upper + 7, // Sky Lower + 8, // Horizon + 1, // Fog Near + 12, // Fog Far + 3, // Ambient + 4, // Sunlight + 15, // Sun Glare + 5, // Sun + 6, // Stars + 16, // Moon Glare + 9, // Effect Lighting + 10, // Cloud LOD Diffuse + 11, // Cloud LOD Ambient + 13, // Sky Statics + 14, // Water Multiplier + 2, // Unknown }; for (int idx = 0; idx < ColorTypes::kTotal; idx++) { @@ -927,80 +931,83 @@ void WeatherWidget::DrawCloudSettings() bool changed = false; for (int i = 0; i < TESWeather::kTotalLayers; i++) { std::string layer = std::format("Layer {}", i); - + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; if (settings.clouds[i].enabled && !settings.clouds[i].texturePath.empty()) { flags |= ImGuiTreeNodeFlags_DefaultOpen; } - if (ImGui::CollapsingHeader(layer.c_str(), flags)) { - ImGui::Indent(10.0f); - ImGui::Spacing(); - - bool layerEnabled = settings.clouds[i].enabled; - - // Begin horizontal layout for enable checkbox and sliders on left, texture on right - ImGui::BeginGroup(); - - if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { - settings.clouds[i].enabled = layerEnabled; - // Always apply cloud enable/disable immediately for instant feedback - EditorWindow::GetSingleton()->PushUndoState(this); - ApplyChanges(); - - // Force weather re-application if locked to make cloud changes visible immediately - if (editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather) { - if (auto sky = RE::Sky::GetSingleton()) { - sky->ForceWeather(weather, false); + if (ImGui::CollapsingHeader(layer.c_str(), flags)) { + ImGui::Indent(10.0f); + ImGui::Spacing(); + + bool layerEnabled = settings.clouds[i].enabled; + + // Begin horizontal layout for enable checkbox and sliders on left, texture on right + ImGui::BeginGroup(); + + if (ImGui::Checkbox(std::format("Enable##{}", layer).c_str(), &layerEnabled)) { + settings.clouds[i].enabled = layerEnabled; + // Always apply cloud enable/disable immediately for instant feedback + EditorWindow::GetSingleton()->PushUndoState(this); + ApplyChanges(); + + // Force weather re-application if locked to make cloud changes visible immediately + if (editorWindow->IsWeatherLocked() && editorWindow->GetLockedWeather() == weather) { + if (auto sky = RE::Sky::GetSingleton()) { + sky->ForceWeather(weather, false); + } } + + changed = true; } - - changed = true; - } - - ImGui::Spacing(); - ImGui::Spacing(); - - // Make sliders 1/3 width - ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.4f); - if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) changed = true; - ImGui::Spacing(); - if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) changed = true; - ImGui::PopItemWidth(); - - ImGui::EndGroup(); - - // Draw texture in upper right if available - if (!settings.clouds[i].texturePath.empty()) { - auto* texture = GetCloudTexture(i); - if (texture) { - ImGui::SameLine(0.0f, 20.0f); - ImGui::BeginGroup(); - float textureSize = 128.0f; - ImGui::Image((void*)texture, ImVec2(textureSize, textureSize)); - // Small grey subtext below image - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); - ImGui::PushFont(ImGui::GetFont()); - ImGui::SetWindowFontScale(0.8f); - float textWidth = ImGui::CalcTextSize(settings.clouds[i].texturePath.c_str()).x; - if (textWidth > textureSize) { - ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + textureSize); - ImGui::TextWrapped("%s", settings.clouds[i].texturePath.c_str()); - ImGui::PopTextWrapPos(); - } else { - ImGui::Text("%s", settings.clouds[i].texturePath.c_str()); + + ImGui::Spacing(); + ImGui::Spacing(); + + // Make sliders 1/3 width + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.4f); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed Y##{}", layer), settings.clouds[i].cloudLayerSpeedY)) + changed = true; + ImGui::Spacing(); + if (WeatherUtils::DrawSliderInt8(std::format("Cloud Layer Speed X##{}", layer), settings.clouds[i].cloudLayerSpeedX)) + changed = true; + ImGui::PopItemWidth(); + + ImGui::EndGroup(); + + // Draw texture in upper right if available + if (!settings.clouds[i].texturePath.empty()) { + auto* texture = GetCloudTexture(i); + if (texture) { + ImGui::SameLine(0.0f, 20.0f); + ImGui::BeginGroup(); + float textureSize = 128.0f; + ImGui::Image((void*)texture, ImVec2(textureSize, textureSize)); + // Small grey subtext below image + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + ImGui::PushFont(ImGui::GetFont()); + ImGui::SetWindowFontScale(0.8f); + float textWidth = ImGui::CalcTextSize(settings.clouds[i].texturePath.c_str()).x; + if (textWidth > textureSize) { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + textureSize); + ImGui::TextWrapped("%s", settings.clouds[i].texturePath.c_str()); + ImGui::PopTextWrapPos(); + } else { + ImGui::Text("%s", settings.clouds[i].texturePath.c_str()); + } + ImGui::SetWindowFontScale(1.0f); + ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::EndGroup(); } - ImGui::SetWindowFontScale(1.0f); - ImGui::PopFont(); - ImGui::PopStyleColor(); - ImGui::EndGroup(); } - } - - ImGui::Spacing(); - ImGui::Spacing(); if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { + + ImGui::Spacing(); + ImGui::Spacing(); + if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { TOD::RenderTODHeader(); - + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Separator(); @@ -1037,7 +1044,7 @@ void WeatherWidget::DrawCloudSettings() TOD::EndTODTable(); } - + ImGui::Spacing(); ImGui::Unindent(10.0f); } @@ -1060,7 +1067,7 @@ void WeatherWidget::DrawFogSettings() ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); ImGui::TableSetupColumn("Day", ImGuiTableColumnFlags_WidthFixed, 250.0f); ImGui::TableSetupColumn("Night", ImGuiTableColumnFlags_WidthFixed, 250.0f); - + // Header row ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -1068,7 +1075,7 @@ void WeatherWidget::DrawFogSettings() ImGui::Text("Day"); ImGui::TableSetColumnIndex(2); ImGui::Text("Night"); - + ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Separator(); @@ -1208,7 +1215,7 @@ void WeatherWidget::DrawProperties(std::string category, std::map(ImGui::GetTime()) - highlightStartTime; @@ -1228,7 +1235,7 @@ void WeatherWidget::DrawProperties(std::string category, std::mapsettings.weatherProperties[property]; } else if (settings.weatherColors.find(property) != settings.weatherColors.end()) { @@ -1293,41 +1306,43 @@ void WeatherWidget::InheritFromParent(const std::string& property) void WeatherWidget::InheritAllFromParent() { - if (!HasParent()) return; - + if (!HasParent()) + return; + WeatherWidget* parentWidget = GetParent(); - if (!parentWidget) return; - + if (!parentWidget) + return; + // Copy all weather properties for (const auto& [key, value] : parentWidget->settings.weatherProperties) { settings.weatherProperties[key] = value; } - + // Copy all weather colors for (const auto& [key, value] : parentWidget->settings.weatherColors) { settings.weatherColors[key] = value; } - + // Copy all fog properties for (const auto& [key, value] : parentWidget->settings.fogProperties) { settings.fogProperties[key] = value; } - + // Copy atmosphere colors for (int i = 0; i < ColorTypes::kTotal; i++) { settings.atmosphereColors[i] = parentWidget->settings.atmosphereColors[i]; } - + // Copy DALC settings for (int i = 0; i < ColorTimes::kTotal; i++) { settings.dalc[i] = parentWidget->settings.dalc[i]; } - + // Copy cloud settings for (int i = 0; i < TESWeather::kTotalLayers; i++) { settings.clouds[i] = parentWidget->settings.clouds[i]; } - + // Set all inherit flags to true settings.inheritFlags["DALC_Specular"] = true; settings.inheritFlags["DALC_Fresnel"] = true; @@ -1337,12 +1352,12 @@ void WeatherWidget::InheritAllFromParent() settings.inheritFlags["DALC_DirYMin"] = true; settings.inheritFlags["DALC_DirZMax"] = true; settings.inheritFlags["DALC_DirZMin"] = true; - + settings.inheritFlags["Fog_Near"] = true; settings.inheritFlags["Fog_Far"] = true; settings.inheritFlags["Fog_Power"] = true; settings.inheritFlags["Fog_Max"] = true; - + // Atmosphere colors static const int displayOrder[] = { 0, 7, 8, 1, 12, 3, 4, 5, 6, 9, 10, 11, 13, 14, 15, 16, 2 }; for (int idx = 0; idx < ColorTypes::kTotal; idx++) { @@ -1350,18 +1365,18 @@ void WeatherWidget::InheritAllFromParent() std::string colorTypeLabel = ColorTypeLabel(i); settings.inheritFlags["Atmosphere_" + colorTypeLabel] = true; } - + // Cloud settings for (int i = 0; i < TESWeather::kTotalLayers; i++) { settings.inheritFlags[std::format("Cloud{}_Color", i)] = true; settings.inheritFlags[std::format("Cloud{}_Alpha", i)] = true; } - + // Apply the changes if (EditorWindow::GetSingleton()->settings.autoApplyChanges) { ApplyChanges(); } - + EditorWindow::GetSingleton()->ShowNotification( std::format("Inherited all settings from {}", parentWidget->GetEditorID()), ImVec4(0.0f, 1.0f, 0.5f, 1.0f), @@ -1413,7 +1428,8 @@ void WeatherWidget::LoadFeatureSettings() if (!missingFeatures.empty()) { std::string missingList; for (size_t i = 0; i < missingFeatures.size(); ++i) { - if (i > 0) missingList += ", "; + if (i > 0) + missingList += ", "; missingList += missingFeatures[i]; } @@ -1634,21 +1650,19 @@ ID3D11ShaderResourceView* WeatherWidget::GetCloudTexture(int layerIndex) // Build resource path for BSA loading: Textures\path (relative to Data folder) // Note: Skyrim texture paths don't include .dds extension, so we add it std::string resourcePath = std::string("Textures\\") + texturePath; - + // Add .dds extension if not present if (resourcePath.size() < 4 || resourcePath.substr(resourcePath.size() - 4) != ".dds") { resourcePath += ".dds"; } - + ID3D11ShaderResourceView* srv = nullptr; ImVec2 textureSize; - + if (Util::LoadDDSTextureFromFile(globals::d3d::device, resourcePath.c_str(), &srv, textureSize)) { cloudTextureCache[layerIndex] = srv; return srv; } - + return nullptr; } - - diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index 8fb43ebf06..eab1135fbe 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -166,10 +166,10 @@ namespace WeatherUtils bool isPreviouslyActive = wasActive[label]; bool changed = ImGui::SliderInt(label.c_str(), &property, -128, 127); - + // Check if item is now active bool isNowActive = ImGui::IsItemActive(); - + // Push undo state only once when slider becomes active (not every frame while dragging) if (isNowActive && !isPreviouslyActive && !undoPushedForSession[label]) { if (g_currentWidget) { @@ -180,15 +180,15 @@ namespace WeatherUtils // Reset undo flag when slider is completely released and idle for a while if (!isNowActive && undoPushedForSession[label]) { - if (lastChangeTime.find(label) == lastChangeTime.end() || - ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { + if (lastChangeTime.find(label) == lastChangeTime.end() || + ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { undoPushedForSession[label] = false; } } // Update active state for next frame wasActive[label] = isNowActive; - + if (changed) { pendingValues[label] = property; lastChangeTime[label] = ImGui::GetTime(); @@ -290,12 +290,12 @@ namespace WeatherUtils // Check if item was active in previous frame bool isPreviouslyActive = wasActive[label]; - + bool changed = ImGui::SliderFloat(label.c_str(), &property, min, max); - + // Check if item is now active bool isNowActive = ImGui::IsItemActive(); - + // Push undo state only once when slider becomes active (not every frame while dragging) if (isNowActive && !isPreviouslyActive && !undoPushedForSession[label]) { // Use parameter if provided, otherwise use global widget @@ -309,15 +309,15 @@ namespace WeatherUtils // Reset undo flag when slider is completely released and idle for a while if (!isNowActive && undoPushedForSession[label]) { // Allow new undo push after slider has been released - if (lastChangeTime.find(label) == lastChangeTime.end() || - ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { + if (lastChangeTime.find(label) == lastChangeTime.end() || + ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { undoPushedForSession[label] = false; } } // Update active state for next frame wasActive[label] = isNowActive; - + if (changed) { pendingValues[label] = property; lastChangeTime[label] = ImGui::GetTime(); @@ -696,7 +696,7 @@ namespace TOD std::string id = std::string("##") + label + std::to_string(i); std::string itemKey = std::string(label) + "_slider_" + std::to_string(i); bool isPreviouslyActive = wasActiveInherit[itemKey]; - + ImGui::BeginDisabled(inheritFlags && inheritFlags[i]); if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { changed = true; @@ -706,7 +706,7 @@ namespace TOD pendingSliderValues[valueName] = values[i]; sliderLastChangeTime[valueName] = ImGui::GetTime(); } - + // Push undo state only once when slider becomes active bool isNowActive = ImGui::IsItemActive(); if (isNowActive && !isPreviouslyActive && !undoPushedInherit[itemKey]) { @@ -715,14 +715,14 @@ namespace TOD undoPushedInherit[itemKey] = true; } } - + // Reset undo flag when slider is released if (!isNowActive && undoPushedInherit[itemKey]) { undoPushedInherit[itemKey] = false; } - + wasActiveInherit[itemKey] = isNowActive; - + ImGui::EndDisabled(); if (ImGui::IsItemHovered()) @@ -922,20 +922,20 @@ namespace TOD static std::map wasActiveMap; static std::map undoPushedMap; - + for (int i = 0; i < Count; ++i) { if (i > 0) ImGui::SameLine(); ImGui::PushID(i); - + std::string itemId = std::string(label) + "_" + std::to_string(i); bool isPreviouslyActive = wasActiveMap[itemId]; - + ImGui::SetNextItemWidth(columnWidth); if (ImGui::SliderFloat("##value", &values[i], minValue, maxValue, format)) { changed = true; } - + // Push undo state only once when slider becomes active bool isNowActive = ImGui::IsItemActive(); if (isNowActive && !isPreviouslyActive && !undoPushedMap[itemId]) { @@ -944,14 +944,14 @@ namespace TOD undoPushedMap[itemId] = true; } } - + // Reset undo flag when slider is released if (!isNowActive && undoPushedMap[itemId]) { undoPushedMap[itemId] = false; } - + wasActiveMap[itemId] = isNowActive; - + ImGui::PopID(); } From d07fb2d3ff0191c26caab96aa4faf261660f2602 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 10 Dec 2025 14:45:54 +1000 Subject: [PATCH 14/30] white --- src/WeatherEditor/Weather/ImageSpaceWidget.cpp | 13 +++++++++++++ src/WeatherEditor/Weather/ImageSpaceWidget.h | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp index fe700230a9..03dcecaaf2 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -12,6 +12,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( hdrBloomBlurRadius, hdrBloomThreshold, hdrBloomScale, + hdrWhite, hdrSunlightScale, hdrSkyScale, cinematicSaturation, @@ -85,6 +86,16 @@ void ImageSpaceWidget::DrawWidget() changed = true; } + if (MatchesSearch("White")) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("White"); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + if (ImGui::SliderFloat("##White", &settings.hdrWhite, 0.0f, 10.0f, "%.3f")) + changed = true; + } + if (MatchesSearch("Sunlight Scale")) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -250,6 +261,7 @@ void ImageSpaceWidget::SetImageSpaceValues() data.hdr.bloomBlurRadius = settings.hdrBloomBlurRadius; data.hdr.bloomThreshold = settings.hdrBloomThreshold; data.hdr.bloomScale = settings.hdrBloomScale; + data.hdr.white = settings.hdrWhite; data.hdr.sunlightScale = settings.hdrSunlightScale; data.hdr.skyScale = settings.hdrSkyScale; @@ -282,6 +294,7 @@ void ImageSpaceWidget::LoadImageSpaceValues() settings.hdrBloomBlurRadius = data.hdr.bloomBlurRadius; settings.hdrBloomThreshold = data.hdr.bloomThreshold; settings.hdrBloomScale = data.hdr.bloomScale; + settings.hdrWhite = data.hdr.white; settings.hdrSunlightScale = data.hdr.sunlightScale; settings.hdrSkyScale = data.hdr.skyScale; diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.h b/src/WeatherEditor/Weather/ImageSpaceWidget.h index 68b27796e3..87961915e8 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.h +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.h @@ -25,9 +25,10 @@ class ImageSpaceWidget : public Widget float hdrBloomBlurRadius = 0.0f; float hdrBloomThreshold = 0.0f; float hdrBloomScale = 0.0f; + float hdrWhite = 0.0f; float hdrSunlightScale = 0.0f; float hdrSkyScale = 0.0f; - + // Cinematic Settings float cinematicSaturation = 0.0f; float cinematicBrightness = 0.0f; From fe676f71ac5112c28de93a34044fceaded25cbbc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:46:27 +0000 Subject: [PATCH 15/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/WeatherEditor/Weather/ImageSpaceWidget.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.h b/src/WeatherEditor/Weather/ImageSpaceWidget.h index 87961915e8..83ddb8f190 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.h +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.h @@ -28,7 +28,7 @@ class ImageSpaceWidget : public Widget float hdrWhite = 0.0f; float hdrSunlightScale = 0.0f; float hdrSkyScale = 0.0f; - + // Cinematic Settings float cinematicSaturation = 0.0f; float cinematicBrightness = 0.0f; From 8d94c87d676968d4309401dc4113bbdd60e89ada Mon Sep 17 00:00:00 2001 From: davo0411 Date: Thu, 11 Dec 2025 07:53:35 +1000 Subject: [PATCH 16/30] Update docs/weather-system-docs/WeatherVariableRegistration.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/weather-system-docs/WeatherVariableRegistration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md index f496e69294..cdb7f0f326 100644 --- a/docs/weather-system-docs/WeatherVariableRegistration.md +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -244,8 +244,6 @@ Uses nlohmann::json for type conversion. Built-in support for: - Type mismatches caught by json exceptions - Invalid weather files are logged but do not crash the system -``` - ## Architecture Benefits ### Separation of Concerns From 65895893a2044d3dd6e2207db002b3c649a6d5bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:53:55 +0000 Subject: [PATCH 17/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- .../WeatherVariableRegistration.md | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/weather-system-docs/WeatherVariableRegistration.md b/docs/weather-system-docs/WeatherVariableRegistration.md index cdb7f0f326..04671712d6 100644 --- a/docs/weather-system-docs/WeatherVariableRegistration.md +++ b/docs/weather-system-docs/WeatherVariableRegistration.md @@ -247,20 +247,27 @@ Uses nlohmann::json for type conversion. Built-in support for: ## Architecture Benefits ### Separation of Concerns -- **Features**: Focus on rendering logic and effect implementation -- **Weather System**: Handles persistence, interpolation, and state management -- **UI Layer**: Automatically discovers registered variables for editor display + +- **Features**: Focus on rendering logic and effect implementation +- **Weather System**: Handles persistence, interpolation, and state management +- **UI Layer**: Automatically discovers registered variables for editor display ### Future Enhancements + The centralized registry enables: -- Weather template inheritance (parent weather settings override children) -- Automatic UI generation for weather variable editing -- Bulk operations (reset all weathers to defaults, copy settings, etc.) -- Variable validation and constraints -- Change tracking and undo/redo support + +- Weather template inheritance (parent weather settings override children) +- Automatic UI generation for weather variable editing +- Bulk operations (reset all weathers to defaults, copy settings, etc.) +- Variable validation and constraints +- Change tracking and undo/redo support ### Performance -- Variables are directly modified in place (no copying) -- Interpolation only happens during weather transitions -- Registration is one-time during feature initialization + +- Variables are directly modified in place (no copying) +- Interpolation only happens during weather transitions +- Registration is one-time during feature initialization + +``` + ``` From dfe27a8d54b7195bd6bf5b702627f0ee9a08eb42 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Thu, 11 Dec 2025 08:37:22 +1000 Subject: [PATCH 18/30] fixes for ai rabbit --- src/WeatherEditor/EditorWindow.cpp | 101 +++++++------------- src/WeatherEditor/EditorWindow.h | 3 + src/WeatherEditor/Weather/WeatherWidget.cpp | 5 +- 3 files changed, 43 insertions(+), 66 deletions(-) diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 234e808143..786d2adb7c 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -666,116 +666,89 @@ void EditorWindow::ShowWidgetWindow() { // Global shortcut for closing focused widget (Ctrl+W) if (ImGui::GetIO().KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_W, false)) { - // Close the most recently focused widget - for (int i = 0; i < (int)weatherWidgets.size(); i++) { - auto& widget = weatherWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } - } - for (int i = 0; i < (int)worldSpaceWidgets.size(); i++) { - auto& widget = worldSpaceWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } - } - for (int i = 0; i < (int)lightingTemplateWidgets.size(); i++) { - auto& widget = lightingTemplateWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } - } - for (int i = 0; i < (int)imageSpaceWidgets.size(); i++) { - auto& widget = imageSpaceWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } - } - for (int i = 0; i < (int)volumetricLightingWidgets.size(); i++) { - auto& widget = volumetricLightingWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } - } - for (int i = 0; i < (int)precipitationWidgets.size(); i++) { - auto& widget = precipitationWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } - } - for (int i = 0; i < (int)lensFlareWidgets.size(); i++) { - auto& widget = lensFlareWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } - } - for (int i = 0; i < (int)referenceEffectWidgets.size(); i++) { - auto& widget = referenceEffectWidgets[i]; - if (widget->IsOpen() && ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)) { - widget->SetOpen(false); - break; - } + if (lastFocusedWidget && lastFocusedWidget->IsOpen()) { + lastFocusedWidget->SetOpen(false); + lastFocusedWidget = nullptr; } } for (int i = 0; i < (int)weatherWidgets.size(); i++) { auto& widget = weatherWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } for (int i = 0; i < (int)worldSpaceWidgets.size(); i++) { auto& widget = worldSpaceWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } for (int i = 0; i < (int)lightingTemplateWidgets.size(); i++) { auto& widget = lightingTemplateWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } for (int i = 0; i < (int)imageSpaceWidgets.size(); i++) { auto& widget = imageSpaceWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } for (int i = 0; i < (int)volumetricLightingWidgets.size(); i++) { auto& widget = volumetricLightingWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } for (int i = 0; i < (int)precipitationWidgets.size(); i++) { auto& widget = precipitationWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } for (int i = 0; i < (int)lensFlareWidgets.size(); i++) { auto& widget = lensFlareWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } for (int i = 0; i < (int)referenceEffectWidgets.size(); i++) { auto& widget = referenceEffectWidgets[i]; - if (widget->IsOpen()) + if (widget->IsOpen()) { widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = widget.get(); + } } // Draw current cell lighting widget if open if (currentCellLightingWidget && currentCellLightingWidget->IsOpen()) { currentCellLightingWidget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) + lastFocusedWidget = currentCellLightingWidget.get(); } } diff --git a/src/WeatherEditor/EditorWindow.h b/src/WeatherEditor/EditorWindow.h index 443cc4c53a..17c73df807 100644 --- a/src/WeatherEditor/EditorWindow.h +++ b/src/WeatherEditor/EditorWindow.h @@ -174,6 +174,9 @@ class EditorWindow bool showSettingsWindow = false; std::string settingsSelectedCategory = "Flags"; + // Widget focus tracking for Ctrl+W + Widget* lastFocusedWidget = nullptr; + // Sorting state enum class SortColumn { diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 92503711da..a67d947d27 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -402,7 +402,8 @@ void WeatherWidget::DrawWidget() ImGui::Spacing(); } - if (recordChanged) { + if (recordChanged && EditorWindow::GetSingleton()->settings.autoApplyChanges) { + ApplyChanges(); } ImGui::EndTabItem(); @@ -655,7 +656,7 @@ void WeatherWidget::LoadWeatherValues() for (size_t i = 0; i < ColorTimes::kTotal; i++) { auto& dalc = weather->directionalAmbientLightingColors[i]; auto& settingsDalc = settings.dalc[i]; - dalc.fresnelPower = settingsDalc.fresnelPower; + settingsDalc.fresnelPower = dalc.fresnelPower; ColorToFloat3(dalc.specular, settingsDalc.specular); From d53079043b9c8bbbd81a9338575897e7c39cb534 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Thu, 11 Dec 2025 18:36:54 +1000 Subject: [PATCH 19/30] fixes --- src/Features/WeatherEditor.cpp | 7 ++++++- src/Features/WeatherEditor.h | 1 + src/WeatherEditor/EditorWindow.cpp | 6 +++--- src/WeatherEditor/Weather/WeatherWidget.cpp | 4 ++++ src/WeatherVariableRegistry.h | 6 +++--- src/XSEPlugin.cpp | 2 -- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp index 5fb517134f..405aa71e05 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/WeatherEditor.cpp @@ -7,6 +7,11 @@ #include "WeatherEditor/EditorWindow.h" +void WeatherEditor::DataLoaded() +{ + EditorWindow::GetSingleton()->SetupResources(); +} + int8_t LerpInt8_t(const int8_t oldValue, const int8_t newVal, const float lerpValue) { int lerpedValue = (int)std::lerp(oldValue, newVal, lerpValue); @@ -22,7 +27,7 @@ uint8_t LerpUint8_t(const uint8_t oldValue, const uint8_t newVal, const float le void LerpColor(const RE::TESWeather::Data::Color3& oldColor, RE::TESWeather::Data::Color3& newColor, const float changePct) { newColor.red = LerpInt8_t(oldColor.red, newColor.red, changePct); - newColor.green = (int8_t)std::lerp(oldColor.green, newColor.green, changePct); + newColor.green = LerpInt8_t(oldColor.green, newColor.green, changePct); newColor.blue = LerpInt8_t(oldColor.blue, newColor.blue, changePct); } diff --git a/src/Features/WeatherEditor.h b/src/Features/WeatherEditor.h index 6768aca20d..53a1e5c095 100644 --- a/src/Features/WeatherEditor.h +++ b/src/Features/WeatherEditor.h @@ -27,6 +27,7 @@ struct WeatherEditor : Feature }; } + virtual void DataLoaded() override; virtual void DrawSettings() override; void LerpWeather(RE::TESWeather*, RE::TESWeather*, float); diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 786d2adb7c..1bfdfece2f 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -1810,7 +1810,7 @@ void EditorWindow::AddToRecent(const std::string& widgetId, const std::string& c categoryRecent.resize(settings.maxRecentWidgets); } - SaveSettings(); + Save(); } void EditorWindow::ToggleFavorite(const std::string& widgetId) @@ -1821,7 +1821,7 @@ void EditorWindow::ToggleFavorite(const std::string& widgetId) } else { settings.favoriteWidgets.push_back(widgetId); } - SaveSettings(); + Save(); } bool EditorWindow::IsFavorite(const std::string& widgetId) const @@ -1850,7 +1850,7 @@ void EditorWindow::SaveSessionWidgets() } } - SaveSettings(); + Save(); } void EditorWindow::RestoreSessionWidgets() diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index a67d947d27..41e1a4ae5a 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -1096,6 +1096,7 @@ void WeatherWidget::DrawFogSettings() if (settings.inheritFlags["Fog_Near"]) { settings.fogProperties["Day Near"] = parentWidget->settings.fogProperties["Day Near"]; settings.fogProperties["Night Near"] = parentWidget->settings.fogProperties["Night Near"]; + changed = true; } ImGui::PopStyleVar(); ImGui::PopStyleColor(2); @@ -1122,6 +1123,7 @@ void WeatherWidget::DrawFogSettings() if (settings.inheritFlags["Fog_Far"]) { settings.fogProperties["Day Far"] = parentWidget->settings.fogProperties["Day Far"]; settings.fogProperties["Night Far"] = parentWidget->settings.fogProperties["Night Far"]; + changed = true; } ImGui::PopStyleVar(); ImGui::PopStyleColor(2); @@ -1148,6 +1150,7 @@ void WeatherWidget::DrawFogSettings() if (settings.inheritFlags["Fog_Power"]) { settings.fogProperties["Day Power"] = parentWidget->settings.fogProperties["Day Power"]; settings.fogProperties["Night Power"] = parentWidget->settings.fogProperties["Night Power"]; + changed = true; } ImGui::PopStyleVar(); ImGui::PopStyleColor(2); @@ -1174,6 +1177,7 @@ void WeatherWidget::DrawFogSettings() if (settings.inheritFlags["Fog_Max"]) { settings.fogProperties["Day Max"] = parentWidget->settings.fogProperties["Day Max"]; settings.fogProperties["Night Max"] = parentWidget->settings.fogProperties["Night Max"]; + changed = true; } ImGui::PopStyleVar(); ImGui::PopStyleColor(2); diff --git a/src/WeatherVariableRegistry.h b/src/WeatherVariableRegistry.h index da6c64637b..03a6260039 100644 --- a/src/WeatherVariableRegistry.h +++ b/src/WeatherVariableRegistry.h @@ -173,10 +173,10 @@ namespace WeatherVariables class FeatureWeatherRegistry { public: - template - void RegisterVariable(std::shared_ptr> var) + template >> + void RegisterVariable(std::shared_ptr var) { - variables.push_back(var); + variables.push_back(std::static_pointer_cast(var)); } void LerpAllVariables(const json& from, const json& to, float factor) diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index d4d6b6c9ed..0fc44c4828 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -8,7 +8,6 @@ #include "ShaderCache.h" #include "State.h" #include "TruePBR.h" -#include "WeatherEditor/EditorWindow.h" #include "ENB/ENBSeriesAPI.h" @@ -111,7 +110,6 @@ void MessageHandler(SKSE::MessagingInterface::Message* message) RE::DebugMessageBox(std::format("Community Shaders\n{}, will disable all hooks and features", errorMessage).c_str()); } - EditorWindow::GetSingleton()->SetupResources(); if (errors.empty()) { globals::OnDataLoaded(); From aa8e9419f3e421125e4133a4f401302b00d76dc2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:37:22 +0000 Subject: [PATCH 20/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/XSEPlugin.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/XSEPlugin.cpp b/src/XSEPlugin.cpp index 0fc44c4828..c3873b3423 100644 --- a/src/XSEPlugin.cpp +++ b/src/XSEPlugin.cpp @@ -110,7 +110,6 @@ void MessageHandler(SKSE::MessagingInterface::Message* message) RE::DebugMessageBox(std::format("Community Shaders\n{}, will disable all hooks and features", errorMessage).c_str()); } - if (errors.empty()) { globals::OnDataLoaded(); EngineFix::InstallOnDataLoadedFixes(); From 72e83b895a557aa855916a639b2753c33e01cb95 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 17 Dec 2025 19:21:09 -0800 Subject: [PATCH 21/30] fix --- src/Features/WeatherEditor.cpp | 5 +++ src/WeatherEditor/Weather/WeatherWidget.cpp | 5 ++- src/WeatherManager.cpp | 38 ++++++++++++++++++--- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/Features/WeatherEditor.cpp b/src/Features/WeatherEditor.cpp index 405aa71e05..d3e94fa181 100644 --- a/src/Features/WeatherEditor.cpp +++ b/src/Features/WeatherEditor.cpp @@ -63,6 +63,11 @@ void WeatherEditor::DrawSettings() void WeatherEditor::LerpWeather(RE::TESWeather* oldWeather, RE::TESWeather* newWeather, float currentWeatherPct) { + if (!oldWeather || !newWeather) { + // Avoid dereferencing null pointers; nothing to lerp. + return; + } + //// Precipitation newWeather->data.precipitationBeginFadeIn = LerpInt8_t(oldWeather->data.precipitationBeginFadeIn, newWeather->data.precipitationBeginFadeIn, currentWeatherPct); newWeather->data.precipitationEndFadeOut = LerpInt8_t(oldWeather->data.precipitationEndFadeOut, newWeather->data.precipitationEndFadeOut, currentWeatherPct); diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 41e1a4ae5a..9dd533bb37 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -1393,9 +1393,8 @@ void WeatherWidget::SaveFeatureSettings() auto* weatherManager = WeatherManager::GetSingleton(); for (const auto& [featureName, featureJson] : settings.featureSettings) { - if (!featureJson.empty()) { - weatherManager->SaveSettingsToWeather(weather, featureName, featureJson); - } + // Always call save so that empty objects are persisted as removals. + weatherManager->SaveSettingsToWeather(weather, featureName, featureJson); } } diff --git a/src/WeatherManager.cpp b/src/WeatherManager.cpp index c7b54ab2c0..6c68fcebe4 100644 --- a/src/WeatherManager.cpp +++ b/src/WeatherManager.cpp @@ -115,8 +115,18 @@ void WeatherManager::SaveSettingsToWeather(RE::TESWeather* weather, const std::s std::string weatherKey = GetWeatherKey(weather); - // Update cache - perWeatherSettingsCache[weatherKey][featureName] = settings; + // Update cache: if settings is empty, remove the feature entry; otherwise set it + if (settings.is_object() && settings.empty()) { + auto wkIt = perWeatherSettingsCache.find(weatherKey); + if (wkIt != perWeatherSettingsCache.end()) { + wkIt->second.erase(featureName); + if (wkIt->second.empty()) { + perWeatherSettingsCache.erase(wkIt); + } + } + } else { + perWeatherSettingsCache[weatherKey][featureName] = settings; + } // Save to disk const std::string weathersPath = std::format("{}\\Weathers", Util::PathHelpers::GetCommunityShaderPath().string()); @@ -147,10 +157,30 @@ void WeatherManager::SaveSettingsToWeather(RE::TESWeather* weather, const std::s } } - // Update with new feature settings - weatherData[featureName] = settings; + // Update with new feature settings or remove feature entry if settings empty + if (settings.is_object() && settings.empty()) { + // Remove feature entry from loaded JSON + if (weatherData.is_object()) { + weatherData.erase(featureName); + } + } else { + weatherData[featureName] = settings; + } // Write back to disk + if (weatherData.is_object() && weatherData.empty()) { + // No features left for this weather — remove file if it exists + if (std::filesystem::exists(filePath)) { + try { + std::filesystem::remove(filePath); + logger::info("Removed weather settings file (no features remain): {}", filePath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to remove empty weather settings file ({}): {}", filePath, e.what()); + } + } + return; + } + std::ofstream settingsFile(filePath); if (!settingsFile.good() || !settingsFile.is_open()) { logger::warn("Failed to open weather settings file for writing: {}", filePath); From 89783203503ea3384387acc41e49c8f3d5fd2f32 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 17 Dec 2025 19:29:09 -0800 Subject: [PATCH 22/30] ai and dev updates --- src/Menu.cpp | 8 ++++++-- src/Menu/IconLoader.cpp | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Menu.cpp b/src/Menu.cpp index 7343e94193..8101b06bec 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -903,8 +903,12 @@ void Menu::ProcessInputEventQueue() } } } - if (key == VK_ESCAPE && IsEnabled && !EditorWindow::GetSingleton()->open) { - IsEnabled = false; + // Guard against a null EditorWindow singleton before accessing `open`. + { + auto* editorWindow = EditorWindow::GetSingleton(); + if (key == VK_ESCAPE && IsEnabled && editorWindow && !editorWindow->open) { + IsEnabled = false; + } } } diff --git a/src/Menu/IconLoader.cpp b/src/Menu/IconLoader.cpp index 884c2f9fa6..951100f49f 100644 --- a/src/Menu/IconLoader.cpp +++ b/src/Menu/IconLoader.cpp @@ -98,9 +98,14 @@ namespace Util::IconLoader { std::string(iconFolder) + "\\save-settings.png", &menu->uiIcons.saveSettings.texture, &menu->uiIcons.saveSettings.size }, { std::string(iconFolder) + "\\load-settings.png", &menu->uiIcons.loadSettings.texture, &menu->uiIcons.loadSettings.size }, { std::string(iconFolder) + "\\clear-cache.png", &menu->uiIcons.clearCache.texture, &menu->uiIcons.clearCache.size }, + { std::string(iconFolder) + "\\delete.png", &menu->uiIcons.deleteSettings.texture, &menu->uiIcons.deleteSettings.size }, { logoPath, &menu->uiIcons.logo.texture, &menu->uiIcons.logo.size }, { std::string(iconFolder) + "\\restore-settings.png", &menu->uiIcons.featureSettingRevert.texture, &menu->uiIcons.featureSettingRevert.size }, { std::string(iconFolder) + "\\discord.png", &menu->uiIcons.discord.texture, &menu->uiIcons.discord.size }, + { std::string(iconFolder) + "\\apply-to-game.png", &menu->uiIcons.applyToGame.texture, &menu->uiIcons.applyToGame.size }, + { std::string(iconFolder) + "\\pause.png", &menu->uiIcons.pauseTime.texture, &menu->uiIcons.pauseTime.size }, + { std::string(iconFolder) + "\\undo.png", &menu->uiIcons.undo.texture, &menu->uiIcons.undo.size }, + { "Categories\\characters.png", &menu->uiIcons.characters.texture, &menu->uiIcons.characters.size }, { "Categories\\display.png", &menu->uiIcons.display.texture, &menu->uiIcons.display.size }, { "Categories\\grass.png", &menu->uiIcons.grass.texture, &menu->uiIcons.grass.size }, @@ -190,8 +195,9 @@ namespace Util::IconLoader auto iconDefs = GetIconDefinitions(menu); for (auto* texturePtr : { &menu->uiIcons.saveSettings.texture, &menu->uiIcons.loadSettings.texture, - &menu->uiIcons.clearCache.texture, &menu->uiIcons.logo.texture, - &menu->uiIcons.featureSettingRevert.texture, &menu->uiIcons.discord.texture, + &menu->uiIcons.clearCache.texture, &menu->uiIcons.deleteSettings.texture, &menu->uiIcons.logo.texture, + &menu->uiIcons.featureSettingRevert.texture, &menu->uiIcons.applyToGame.texture, &menu->uiIcons.pauseTime.texture, + &menu->uiIcons.undo.texture, &menu->uiIcons.search.texture, &menu->uiIcons.discord.texture, &menu->uiIcons.characters.texture, &menu->uiIcons.display.texture, &menu->uiIcons.grass.texture, &menu->uiIcons.lighting.texture, &menu->uiIcons.sky.texture, &menu->uiIcons.landscape.texture, From d71d265b3f923971f5148419ddedffb68f47d9a1 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 17 Dec 2025 19:38:31 -0800 Subject: [PATCH 23/30] updated to new theme stuff (UNTESTED) --- src/WeatherEditor/EditorWindow.cpp | 32 +++++++------ src/WeatherEditor/Weather/WeatherWidget.cpp | 8 ++-- src/WeatherEditor/WeatherUtils.cpp | 3 +- src/WeatherEditor/Widget.cpp | 52 +++++++++++++-------- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 1bfdfece2f..2711242096 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -6,6 +6,7 @@ #include "State.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" +#include "Utils/UI.h" #include "imgui_internal.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, enableInheritFromParent, editorUIScale, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) @@ -360,7 +361,7 @@ void EditorWindow::ShowObjectsWindow() } // Highlight current cell - auto highlightColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor; + auto highlightColor = Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor; highlightColor.w = 0.3f; ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::ColorConvertFloat4ToU32(highlightColor)); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::ColorConvertFloat4ToU32(highlightColor)); @@ -390,14 +391,14 @@ void EditorWindow::ShowObjectsWindow() // Show message that cell lighting is only for interior cells ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Warning, "Cell Lighting is only available for interior cells."); - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Disable, "You are currently in an exterior cell."); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Warning, "Cell Lighting is only available for interior cells."); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Disable, "You are currently in an exterior cell."); } } else { // No player or cell ImGui::TableNextRow(); ImGui::TableSetColumnIndex(1); - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Error, "Player cell not available."); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.Error, "Player cell not available."); } } @@ -550,7 +551,7 @@ void EditorWindow::ShowObjectsWindow() ImGui::BeginTooltip(); // ImageSpace info - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "ImageSpace:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "ImageSpace:"); for (int tod = 0; tod < 4; tod++) { auto imgSpace = weatherWidget->weather->imageSpaces[tod]; ImGui::Text(" %s: %s", @@ -561,7 +562,7 @@ void EditorWindow::ShowObjectsWindow() ImGui::Spacing(); // VolumetricLighting info - ImGui::TextColored(Menu::GetSingleton()->GetSettings().Theme.StatusPalette.InfoColor, "Volumetric Lighting:"); + ImGui::TextColored(Menu::GetSingleton()->GetTheme().StatusPalette.InfoColor, "Volumetric Lighting:"); for (int tod = 0; tod < 4; tod++) { auto volLight = weatherWidget->weather->volumetricLighting[tod]; ImGui::Text(" %s: %s", @@ -1397,7 +1398,7 @@ void EditorWindow::ShowSettingsWindow() } AddTooltip("Scale the size of all editor UI elements (0.5 = 50%, 2.0 = 200%)"); - if (ImGui::Button("Reset to 1.0")) { + if (Util::ButtonWithFlash("Reset to 1.0")) { settings.editorUIScale = 1.0f; Save(); } @@ -1414,12 +1415,12 @@ void EditorWindow::ShowSettingsWindow() ImGui::SliderInt("Max recent widgets", &settings.maxRecentWidgets, 5, 20); AddTooltip("Maximum number of recent widgets to remember"); - if (ImGui::Button("Clear Recent History")) { + if (Util::ButtonWithFlash("Clear Recent History")) { settings.recentWidgets.clear(); Save(); } ImGui::SameLine(); - if (ImGui::Button("Clear Favorites")) { + if (Util::ButtonWithFlash("Clear Favorites")) { settings.favoriteWidgets.clear(); Save(); } @@ -1467,13 +1468,16 @@ void EditorWindow::ShowSettingsWindow() } ImGui::TableSetColumnIndex(2); - auto deleteColor = Menu::GetSingleton()->GetSettings().Theme.StatusPalette.Warning; + auto deleteColor = Menu::GetSingleton()->GetTheme().StatusPalette.Warning; deleteColor.y = deleteColor.y * 0.5f; - ImGui::PushStyleColor(ImGuiCol_Button, deleteColor); - if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { - markerToDelete = recordMarker.first; + auto deleteHovered = deleteColor; deleteHovered.w = 0.8f; + auto deleteActive = deleteColor; deleteActive.w = 1.0f; + { + auto styledButton = Util::StyledButtonWrapper(deleteColor, deleteHovered, deleteActive); + if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { + markerToDelete = recordMarker.first; + } } - ImGui::PopStyleColor(); } // Process rename diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index 9dd533bb37..be42be0085 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -153,7 +153,7 @@ void WeatherWidget::DrawWidget() if (parent) { ImGui::SameLine(); - if (ImGui::Button("Inherit All")) { + if (Util::ButtonWithFlash("Inherit All")) { InheritAllFromParent(); } if (ImGui::IsItemHovered()) { @@ -162,7 +162,7 @@ void WeatherWidget::DrawWidget() if (!parent->IsOpen()) { ImGui::SameLine(); - if (ImGui::Button("Open")) + if (Util::ButtonWithFlash("Open")) parent->SetOpen(true); } } @@ -1525,11 +1525,11 @@ void WeatherWidget::DrawFeatureSettings() if (hasSettings) { ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); - if (ImGui::Button("Clear Settings")) { +if (Util::ButtonWithFlash("Clear Settings")) { settings.featureSettings[featureName] = json::object(); } ImGui::SameLine(); - if (ImGui::Button("View JSON")) { + if (Util::ButtonWithFlash("View JSON")) { ImGui::OpenPopup("FeatureJSON"); } diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index eab1135fbe..22c6e72c77 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -1,6 +1,7 @@ #include "WeatherUtils.h" #include "EditorWindow.h" #include "PaletteWindow.h" +#include "Utils/UI.h" // Global widget context for undo tracking static Widget* g_currentWidget = nullptr; @@ -1099,7 +1100,7 @@ bool BeginWidgetSearchBar(char* searchBuffer, size_t bufferSize, bool& searchAct // Clear button ImGui::SameLine(); - if (ImGui::Button("Clear", ImVec2(90, 0))) { + if (Util::ButtonWithFlash("Clear", ImVec2(90, 0))) { searchBuffer[0] = '\0'; } diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index f5182247ef..07dfaad06c 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -2,6 +2,7 @@ #include "EditorWindow.h" #include "State.h" #include "Util.h" +#include "Utils/UI.h" #include "WeatherUtils.h" bool Widget::MatchesSearch(const std::string& text) const @@ -421,11 +422,15 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s applySize.x += ImGui::GetStyle().FramePadding.x * 2.0f; applySize.y = buttonHeight; - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.9f, 1.0f)); - if (ImGui::Button("Apply", applySize)) { - ApplyChanges(); + auto successColor = Menu::GetSingleton()->GetTheme().StatusPalette.SuccessColor; + auto successHover = successColor; successHover.w = 0.8f; + auto successActive = successColor; successActive.w = 1.0f; + { + auto styledButton = Util::StyledButtonWrapper(successColor, successHover, successActive); + if (ImGui::Button("Apply", applySize)) { + ApplyChanges(); + } } - ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Apply changes to the game"); } @@ -436,11 +441,15 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s revertSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; revertSize.y = buttonHeight; - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - if (ImGui::Button("Revert", revertSize)) { - RevertChanges(); + auto warningColor = Menu::GetSingleton()->GetTheme().StatusPalette.Warning; + auto warningHover = warningColor; warningHover.w = 0.8f; + auto warningActive = warningColor; warningActive.w = 1.0f; + { + auto styledButton = Util::StyledButtonWrapper(warningColor, warningHover, warningActive); + if (ImGui::Button("Revert", revertSize)) { + RevertChanges(); + } } - ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Revert to saved values"); } @@ -454,7 +463,7 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s saveSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; saveSize.y = buttonHeight; - if (ImGui::Button("Save", saveSize)) { + if (Util::ButtonWithFlash("Save", saveSize)) { Save(); } if (ImGui::IsItemHovered()) { @@ -467,7 +476,7 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s loadSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; loadSize.y = buttonHeight; - if (ImGui::Button("Load", loadSize)) { + if (Util::ButtonWithFlash("Load", loadSize)) { Load(); } if (ImGui::IsItemHovered()) { @@ -481,16 +490,19 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s deleteSize.x += ImGui::GetStyle().FramePadding.x * 2.0f; deleteSize.y = buttonHeight; - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.3f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.4f, 0.3f, 1.0f)); - if (ImGui::Button("Delete", deleteSize)) { - if (editorWindow->settings.suppressDeleteWarning) { - Delete(); - } else { - ImGui::OpenPopup("ConfirmDelete"); + auto errorColor = Menu::GetSingleton()->GetTheme().StatusPalette.Error; + auto errorHover = errorColor; errorHover.w = 0.8f; + auto errorActive = errorColor; errorActive.w = 1.0f; + { + auto styledButton = Util::StyledButtonWrapper(errorColor, errorHover, errorActive); + if (ImGui::Button("Delete", deleteSize)) { + if (editorWindow->settings.suppressDeleteWarning) { + Delete(); + } else { + ImGui::OpenPopup("ConfirmDelete"); + } } } - ImGui::PopStyleColor(2); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Delete saved file and revert to defaults"); } @@ -512,13 +524,13 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s ImGui::Spacing(); - if (ImGui::Button("Yes", ImVec2(120, 0))) { + if (Util::ButtonWithFlash("Yes", ImVec2(120, 0))) { Delete(); ImGui::CloseCurrentPopup(); } ImGui::SetItemDefaultFocus(); ImGui::SameLine(); - if (ImGui::Button("No", ImVec2(120, 0))) { + if (Util::ButtonWithFlash("No", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); From f4a0cc37b97427c7728302fdb7c96dd42d15b479 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 03:38:58 +0000 Subject: [PATCH 24/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/WeatherEditor/EditorWindow.cpp | 8 +++++--- src/WeatherEditor/Weather/WeatherWidget.cpp | 2 +- src/WeatherEditor/Widget.cpp | 18 ++++++++++++------ 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index 2711242096..ffb1e63883 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -4,9 +4,9 @@ #include "Menu.h" #include "PaletteWindow.h" #include "State.h" +#include "Utils/UI.h" #include "Weather/LightingTemplateWidget.h" #include "WeatherUtils.h" -#include "Utils/UI.h" #include "imgui_internal.h" NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EditorWindow::Settings, recordMarkers, markedRecords, autoApplyChanges, suppressDeleteWarning, useTextButtons, enableInheritFromParent, editorUIScale, favoriteWidgets, recentWidgets, maxRecentWidgets, rememberOpenWidgets, lastOpenWidgets) @@ -1470,8 +1470,10 @@ void EditorWindow::ShowSettingsWindow() ImGui::TableSetColumnIndex(2); auto deleteColor = Menu::GetSingleton()->GetTheme().StatusPalette.Warning; deleteColor.y = deleteColor.y * 0.5f; - auto deleteHovered = deleteColor; deleteHovered.w = 0.8f; - auto deleteActive = deleteColor; deleteActive.w = 1.0f; + auto deleteHovered = deleteColor; + deleteHovered.w = 0.8f; + auto deleteActive = deleteColor; + deleteActive.w = 1.0f; { auto styledButton = Util::StyledButtonWrapper(deleteColor, deleteHovered, deleteActive); if (ImGui::Button(std::format("Delete##{}", recordMarker.first).c_str(), ImVec2(-1, 0))) { diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index be42be0085..b5620e3ef2 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -1525,7 +1525,7 @@ void WeatherWidget::DrawFeatureSettings() if (hasSettings) { ImGui::TextColored({ 0.0f, 1.0f, 0.0f, 1.0f }, "Has weather-specific settings"); -if (Util::ButtonWithFlash("Clear Settings")) { + if (Util::ButtonWithFlash("Clear Settings")) { settings.featureSettings[featureName] = json::object(); } ImGui::SameLine(); diff --git a/src/WeatherEditor/Widget.cpp b/src/WeatherEditor/Widget.cpp index 07dfaad06c..7e588da6d8 100644 --- a/src/WeatherEditor/Widget.cpp +++ b/src/WeatherEditor/Widget.cpp @@ -423,8 +423,10 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s applySize.y = buttonHeight; auto successColor = Menu::GetSingleton()->GetTheme().StatusPalette.SuccessColor; - auto successHover = successColor; successHover.w = 0.8f; - auto successActive = successColor; successActive.w = 1.0f; + auto successHover = successColor; + successHover.w = 0.8f; + auto successActive = successColor; + successActive.w = 1.0f; { auto styledButton = Util::StyledButtonWrapper(successColor, successHover, successActive); if (ImGui::Button("Apply", applySize)) { @@ -442,8 +444,10 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s revertSize.y = buttonHeight; auto warningColor = Menu::GetSingleton()->GetTheme().StatusPalette.Warning; - auto warningHover = warningColor; warningHover.w = 0.8f; - auto warningActive = warningColor; warningActive.w = 1.0f; + auto warningHover = warningColor; + warningHover.w = 0.8f; + auto warningActive = warningColor; + warningActive.w = 1.0f; { auto styledButton = Util::StyledButtonWrapper(warningColor, warningHover, warningActive); if (ImGui::Button("Revert", revertSize)) { @@ -491,8 +495,10 @@ void Widget::DrawWidgetHeader(const char* searchId, bool showApplyRevert, bool s deleteSize.y = buttonHeight; auto errorColor = Menu::GetSingleton()->GetTheme().StatusPalette.Error; - auto errorHover = errorColor; errorHover.w = 0.8f; - auto errorActive = errorColor; errorActive.w = 1.0f; + auto errorHover = errorColor; + errorHover.w = 0.8f; + auto errorActive = errorColor; + errorActive.w = 1.0f; { auto styledButton = Util::StyledButtonWrapper(errorColor, errorHover, errorActive); if (ImGui::Button("Delete", deleteSize)) { From a977f18bb86c0bcd0a0852f526e3d3e2e112689a Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Wed, 17 Dec 2025 19:42:53 -0800 Subject: [PATCH 25/30] fix --- src/WeatherManager.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/WeatherManager.cpp b/src/WeatherManager.cpp index 6c68fcebe4..2dd2d14e8f 100644 --- a/src/WeatherManager.cpp +++ b/src/WeatherManager.cpp @@ -88,14 +88,14 @@ void WeatherManager::UpdateFeatures() json currWeatherSettings; json nextWeatherSettings; - // Load settings for current weather - if (currentWeathers.currentWeather) { - LoadSettingsFromWeather(currentWeathers.currentWeather, featureName, currWeatherSettings); + // Load settings for last weather (from) + if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { + LoadSettingsFromWeather(currentWeathers.lastWeather, featureName, currWeatherSettings); } - // Load settings for transitioning weather - if (currentWeathers.lastWeather && currentWeathers.lerpFactor < 1.0f) { - LoadSettingsFromWeather(currentWeathers.lastWeather, featureName, nextWeatherSettings); + // Load settings for current weather (to) + if (currentWeathers.currentWeather) { + LoadSettingsFromWeather(currentWeathers.currentWeather, featureName, nextWeatherSettings); } // Let the global registry handle variable interpolation From c8370876a6552fd9115935ce992e9ca696dc13a5 Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 20 Dec 2025 09:41:36 -0800 Subject: [PATCH 26/30] Update UI.cpp --- src/Utils/UI.cpp | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 9be4131f8e..952619e0dc 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -1176,6 +1176,85 @@ namespace Util return clicked; } + bool LoadDDSTextureFromFile(ID3D11Device* device, + const char* filename, + ID3D11ShaderResourceView** out_srv, + ImVec2& out_size) + { + if (!device || !out_srv) { + logger::warn("LoadDDSTextureFromFile: Invalid parameters"); + return false; + } + + *out_srv = nullptr; + + // Try to load from BSA using Skyrim's resource system + RE::BSResourceNiBinaryStream bsaStream(filename); + if (!bsaStream.good()) { + logger::warn("LoadDDSTextureFromFile: Failed to open resource: {}", filename); + return false; + } + + // Read entire DDS file into memory + std::vector ddsData; + auto size = bsaStream.stream->totalSize; + if (size == 0) { + logger::warn("LoadDDSTextureFromFile: Resource has zero size: {}", filename); + return false; + } + + ddsData.resize(size); + bsaStream.read(reinterpret_cast(ddsData.data()), size); + + // Load DDS from memory + DirectX::ScratchImage image; + try { + DX::ThrowIfFailed(DirectX::LoadFromDDSMemory( + ddsData.data(), + ddsData.size(), + DirectX::DDS_FLAGS_NONE, + nullptr, + image)); + } catch (const DX::com_exception& e) { + logger::warn("LoadDDSTextureFromFile: Failed to load DDS data from {}: {}", filename, e.what()); + return false; + } + + ID3D11Resource* pResource = nullptr; + try { + DX::ThrowIfFailed(DirectX::CreateTexture(device, + image.GetImages(), image.GetImageCount(), + image.GetMetadata(), &pResource)); + } catch (const DX::com_exception& e) { + logger::warn("LoadDDSTextureFromFile: Failed to create texture: {}", e.what()); + return false; + } + + ID3D11Texture2D* pTexture = reinterpret_cast(pResource); + D3D11_TEXTURE2D_DESC desc; + pTexture->GetDesc(&desc); + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = { + .Format = desc.Format, + .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D, + .Texture2D = { + .MostDetailedMip = 0, + .MipLevels = desc.MipLevels } + }; + + HRESULT hr = device->CreateShaderResourceView(pTexture, &srvDesc, out_srv); + pTexture->Release(); + + if (FAILED(hr) || !*out_srv) { + logger::warn("LoadDDSTextureFromFile: Failed to create SRV, HRESULT: 0x{:08X}", static_cast(hr)); + return false; + } + + out_size = ImVec2((float)desc.Width, (float)desc.Height); + logger::debug("LoadDDSTextureFromFile: Successfully loaded {} ({}x{})", filename, desc.Width, desc.Height); + return true; + } + bool FeatureToggle(const char* label, bool* enabled, const ImVec2& size) { if (!enabled) From b27f47319e22069fa9b06a7f68a30189864664ff Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sat, 20 Dec 2025 10:39:40 -0800 Subject: [PATCH 27/30] refactor, cut line count --- src/WeatherEditor/EditorWindow.cpp | 183 ++--------- .../Weather/ImageSpaceWidget.cpp | 190 ++--------- .../Weather/LightingTemplateWidget.cpp | 6 +- src/WeatherEditor/Weather/WeatherWidget.cpp | 27 +- src/WeatherEditor/WeatherUtils.cpp | 296 +++++++++--------- src/WeatherEditor/WeatherUtils.h | 178 +++++++++++ 6 files changed, 381 insertions(+), 499 deletions(-) diff --git a/src/WeatherEditor/EditorWindow.cpp b/src/WeatherEditor/EditorWindow.cpp index ffb1e63883..e3218876bb 100644 --- a/src/WeatherEditor/EditorWindow.cpp +++ b/src/WeatherEditor/EditorWindow.cpp @@ -673,77 +673,15 @@ void EditorWindow::ShowWidgetWindow() } } - for (int i = 0; i < (int)weatherWidgets.size(); i++) { - auto& widget = weatherWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } - - for (int i = 0; i < (int)worldSpaceWidgets.size(); i++) { - auto& widget = worldSpaceWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } - - for (int i = 0; i < (int)lightingTemplateWidgets.size(); i++) { - auto& widget = lightingTemplateWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } - - for (int i = 0; i < (int)imageSpaceWidgets.size(); i++) { - auto& widget = imageSpaceWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } - - for (int i = 0; i < (int)volumetricLightingWidgets.size(); i++) { - auto& widget = volumetricLightingWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } - - for (int i = 0; i < (int)precipitationWidgets.size(); i++) { - auto& widget = precipitationWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } - - for (int i = 0; i < (int)lensFlareWidgets.size(); i++) { - auto& widget = lensFlareWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } - - for (int i = 0; i < (int)referenceEffectWidgets.size(); i++) { - auto& widget = referenceEffectWidgets[i]; - if (widget->IsOpen()) { - widget->DrawWidget(); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) - lastFocusedWidget = widget.get(); - } - } + // Draw all open widgets using WidgetFactory template + WidgetFactory::DrawOpenWidgets(weatherWidgets, lastFocusedWidget); + WidgetFactory::DrawOpenWidgets(worldSpaceWidgets, lastFocusedWidget); + WidgetFactory::DrawOpenWidgets(lightingTemplateWidgets, lastFocusedWidget); + WidgetFactory::DrawOpenWidgets(imageSpaceWidgets, lastFocusedWidget); + WidgetFactory::DrawOpenWidgets(volumetricLightingWidgets, lastFocusedWidget); + WidgetFactory::DrawOpenWidgets(precipitationWidgets, lastFocusedWidget); + WidgetFactory::DrawOpenWidgets(lensFlareWidgets, lastFocusedWidget); + WidgetFactory::DrawOpenWidgets(referenceEffectWidgets, lastFocusedWidget); // Draw current cell lighting widget if open if (currentCellLightingWidget && currentCellLightingWidget->IsOpen()) { @@ -1172,99 +1110,22 @@ EditorWindow::~EditorWindow() void EditorWindow::SetupResources() { - auto dataHandler = RE::TESDataHandler::GetSingleton(); - auto& weatherArray = dataHandler->GetFormArray(); - Load(); PaletteWindow::GetSingleton()->Load(); - for (auto weather : weatherArray) { - auto widget = std::make_unique(weather); - widget->CacheFormData(); - widget->Load(); - weatherWidgets.push_back(std::move(widget)); - } - - auto& worldSpaceArray = dataHandler->GetFormArray(); - - for (auto worldSpace : worldSpaceArray) { - auto widget = std::make_unique(worldSpace); - widget->CacheFormData(); - widget->Load(); - worldSpaceWidgets.push_back(std::move(widget)); - } - - auto& lightingTemplateArray = dataHandler->GetFormArray(); - - for (auto lightingTemplate : lightingTemplateArray) { - auto widget = std::make_unique(lightingTemplate); - widget->CacheFormData(); - widget->Load(); - lightingTemplateWidgets.push_back(std::move(widget)); - } - - auto& imageSpaceArray = dataHandler->GetFormArray(); - - for (auto imageSpace : imageSpaceArray) { - auto widget = std::make_unique(imageSpace); - widget->CacheFormData(); - widget->Load(); - imageSpaceWidgets.push_back(std::move(widget)); - } - - auto& volumetricLightingArray = dataHandler->GetFormArray(); - - for (auto volumetricLighting : volumetricLightingArray) { - auto widget = std::make_unique(volumetricLighting); - widget->CacheFormData(); - widget->Load(); - volumetricLightingWidgets.push_back(std::move(widget)); - } - - auto& precipitationArray = dataHandler->GetFormArray(); - - for (auto precipitation : precipitationArray) { - auto widget = std::make_unique(precipitation); - widget->CacheFormData(); - widget->Load(); - precipitationWidgets.push_back(std::move(widget)); - } - - auto& lensFlareArray = dataHandler->GetFormArray(); - - for (auto lensFlare : lensFlareArray) { - auto widget = std::make_unique(lensFlare); - widget->CacheFormData(); - widget->Load(); - lensFlareWidgets.push_back(std::move(widget)); - } - - auto& referenceEffectArray = dataHandler->GetFormArray(); - - for (auto referenceEffect : referenceEffectArray) { - auto widget = std::make_unique(referenceEffect); - widget->CacheFormData(); - widget->Load(); - referenceEffectWidgets.push_back(std::move(widget)); - } - - // Cache art objects for form picker performance - auto& artObjectArray = dataHandler->GetFormArray(); - for (auto artObject : artObjectArray) { - auto widget = std::make_unique(); - widget->form = artObject; - widget->CacheFormData(); - artObjectWidgets.push_back(std::move(widget)); - } - - // Cache effect shaders for form picker performance - auto& effectShaderArray = dataHandler->GetFormArray(); - for (auto effectShader : effectShaderArray) { - auto widget = std::make_unique(); - widget->form = effectShader; - widget->CacheFormData(); - effectShaderWidgets.push_back(std::move(widget)); - } + // Populate all widget collections using WidgetFactory templates + WidgetFactory::PopulateWidgets(weatherWidgets); + WidgetFactory::PopulateWidgets(worldSpaceWidgets); + WidgetFactory::PopulateWidgets(lightingTemplateWidgets); + WidgetFactory::PopulateWidgets(imageSpaceWidgets); + WidgetFactory::PopulateWidgets(volumetricLightingWidgets); + WidgetFactory::PopulateWidgets(precipitationWidgets); + WidgetFactory::PopulateWidgets(lensFlareWidgets); + WidgetFactory::PopulateWidgets(referenceEffectWidgets); + + // Cache simple form widgets for form picker performance + WidgetFactory::PopulateSimpleWidgets(artObjectWidgets); + WidgetFactory::PopulateSimpleWidgets(effectShaderWidgets); } void EditorWindow::Draw() diff --git a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp index 03dcecaaf2..e79d20f973 100644 --- a/src/WeatherEditor/Weather/ImageSpaceWidget.cpp +++ b/src/WeatherEditor/Weather/ImageSpaceWidget.cpp @@ -39,188 +39,40 @@ void ImageSpaceWidget::DrawWidget() DrawWidgetHeader("##ImageSpaceSearch", false, true); // Draw all settings in a unified table - if (ImGui::BeginTable("ImageSpaceSettings", 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { - ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, 200.0f); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - + if (PropertyDrawer::BeginTable("ImageSpaceSettings", 200.0f)) { bool changed = false; + const char* search = searchBuffer[0] ? searchBuffer : nullptr; // HDR Settings - if (MatchesSearch("Eye Adapt Speed")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Eye Adapt Speed"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##EyeAdaptSpeed", &settings.hdrEyeAdaptSpeed, 0.0f, 10.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("Bloom Blur Radius")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Bloom Blur Radius"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##BloomBlurRadius", &settings.hdrBloomBlurRadius, 0.0f, 10.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("Bloom Threshold")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Bloom Threshold"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##BloomThreshold", &settings.hdrBloomThreshold, 0.0f, 10.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("Bloom Scale")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Bloom Scale"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##BloomScale", &settings.hdrBloomScale, 0.0f, 10.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("White")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("White"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##White", &settings.hdrWhite, 0.0f, 10.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("Sunlight Scale")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Sunlight Scale"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##SunlightScale", &settings.hdrSunlightScale, 0.0f, 10.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("Sky Scale")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Sky Scale"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##SkyScale", &settings.hdrSkyScale, 0.0f, 10.0f, "%.3f")) - changed = true; - } + changed |= PropertyDrawer::DrawFloat("Eye Adapt Speed", settings.hdrEyeAdaptSpeed, 0.0f, 10.0f, search); + changed |= PropertyDrawer::DrawFloat("Bloom Blur Radius", settings.hdrBloomBlurRadius, 0.0f, 10.0f, search); + changed |= PropertyDrawer::DrawFloat("Bloom Threshold", settings.hdrBloomThreshold, 0.0f, 10.0f, search); + changed |= PropertyDrawer::DrawFloat("Bloom Scale", settings.hdrBloomScale, 0.0f, 10.0f, search); + changed |= PropertyDrawer::DrawFloat("White", settings.hdrWhite, 0.0f, 10.0f, search); + changed |= PropertyDrawer::DrawFloat("Sunlight Scale", settings.hdrSunlightScale, 0.0f, 10.0f, search); + changed |= PropertyDrawer::DrawFloat("Sky Scale", settings.hdrSkyScale, 0.0f, 10.0f, search); - // Separator between sections - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + PropertyDrawer::DrawSeparator(); // Cinematic Settings - if (MatchesSearch("Saturation")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Saturation"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##Saturation", &settings.cinematicSaturation, 0.0f, 2.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("Brightness")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Brightness"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##Brightness", &settings.cinematicBrightness, 0.0f, 2.0f, "%.3f")) - changed = true; - } + changed |= PropertyDrawer::DrawFloat("Saturation", settings.cinematicSaturation, 0.0f, 2.0f, search); + changed |= PropertyDrawer::DrawFloat("Brightness", settings.cinematicBrightness, 0.0f, 2.0f, search); + changed |= PropertyDrawer::DrawFloat("Contrast", settings.cinematicContrast, 0.0f, 2.0f, search); - if (MatchesSearch("Contrast")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Contrast"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##Contrast", &settings.cinematicContrast, 0.0f, 2.0f, "%.3f")) - changed = true; - } - - // Separator - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + PropertyDrawer::DrawSeparator(); // Tint Settings - if (MatchesSearch("Tint Color")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Tint Color"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (WeatherUtils::DrawColorEdit("Tint Color", settings.tintColor)) - changed = true; - } + changed |= PropertyDrawer::DrawColor("Tint Color", settings.tintColor, search); + changed |= PropertyDrawer::DrawFloat("Tint Amount", settings.tintAmount, 0.0f, 1.0f, search); - if (MatchesSearch("Tint Amount")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("Tint Amount"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##TintAmount", &settings.tintAmount, 0.0f, 1.0f, "%.3f")) - changed = true; - } - - // Separator - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + PropertyDrawer::DrawSeparator(); // Depth of Field - if (MatchesSearch("DOF Strength")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("DOF Strength"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##DOFStrength", &settings.dofStrength, 0.0f, 10.0f, "%.3f")) - changed = true; - } - - if (MatchesSearch("DOF Distance")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("DOF Distance"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##DOFDistance", &settings.dofDistance, 0.0f, 10000.0f, "%.1f")) - changed = true; - } - - if (MatchesSearch("DOF Range")) { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Text("DOF Range"); - ImGui::TableSetColumnIndex(1); - ImGui::SetNextItemWidth(-1); - if (ImGui::SliderFloat("##DOFRange", &settings.dofRange, 0.0f, 10000.0f, "%.1f")) - changed = true; - } + changed |= PropertyDrawer::DrawFloat("DOF Strength", settings.dofStrength, 0.0f, 10.0f, search); + changed |= PropertyDrawer::DrawFloat("DOF Distance", settings.dofDistance, 0.0f, 10000.0f, search, "%.1f"); + changed |= PropertyDrawer::DrawFloat("DOF Range", settings.dofRange, 0.0f, 10000.0f, search, "%.1f"); - ImGui::EndTable(); + PropertyDrawer::EndTable(); if (changed && editorWindow->settings.autoApplyChanges) { ApplyChanges(); diff --git a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp index 5d9b32aae2..a3297399c6 100644 --- a/src/WeatherEditor/Weather/LightingTemplateWidget.cpp +++ b/src/WeatherEditor/Weather/LightingTemplateWidget.cpp @@ -172,11 +172,7 @@ void LightingTemplateWidget::DrawDALCSettings() if (TOD::BeginTODTable("DALCDirectionalTable")) { TOD::RenderTODHeader(); - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + TOD::DrawTODSeparator(); // Prepare arrays for TOD rendering (map X,Y,Z to Sunrise,Day,Sunset,Night) float3 maxColors[4]; diff --git a/src/WeatherEditor/Weather/WeatherWidget.cpp b/src/WeatherEditor/Weather/WeatherWidget.cpp index b5620e3ef2..645b4f166b 100644 --- a/src/WeatherEditor/Weather/WeatherWidget.cpp +++ b/src/WeatherEditor/Weather/WeatherWidget.cpp @@ -699,12 +699,7 @@ void WeatherWidget::DrawDALCSettings() if (TOD::BeginTODTable("DALC_TOD_Table")) { TOD::RenderTODHeader(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + TOD::DrawTODSeparator(); // Prepare arrays for TOD rendering float3 specularColors[4]; @@ -769,11 +764,7 @@ void WeatherWidget::DrawDALCSettings() } } - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + TOD::DrawTODSeparator(); // Directional colors with per-parameter inheritance if (hasParent) { @@ -867,12 +858,7 @@ void WeatherWidget::DrawWeatherColorSettings() if (TOD::BeginTODTable("AtmosphereColors_Table")) { TOD::RenderTODHeader(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + TOD::DrawTODSeparator(); // Organized display order: group related sky/fog/lighting properties static const int displayOrder[] = { @@ -1008,12 +994,7 @@ void WeatherWidget::DrawCloudSettings() ImGui::Spacing(); if (TOD::BeginTODTable((layer + "_TOD_Table").c_str())) { TOD::RenderTODHeader(); - - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::Separator(); - ImGui::TableSetColumnIndex(1); - ImGui::Separator(); + TOD::DrawTODSeparator(); if (hasParent) { float3 parentColors[4]; diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index 22c6e72c77..efde65275e 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -155,59 +155,33 @@ namespace WeatherUtils g_currentWidget = widget; } + // Static debounced trackers for undo and palette tracking + static DebouncedTracker s_int8Tracker; + static DebouncedTracker s_floatTracker; + bool DrawSliderInt8(const std::string& label, int& property) { - static std::map pendingValues; - static std::map lastChangeTime; - static std::map wasActive; - static std::map undoPushedForSession; const double debounceDelay = 2.0; - - // Check if item was active in previous frame - bool isPreviouslyActive = wasActive[label]; + double currentTime = ImGui::GetTime(); bool changed = ImGui::SliderInt(label.c_str(), &property, -128, 127); - - // Check if item is now active bool isNowActive = ImGui::IsItemActive(); - // Push undo state only once when slider becomes active (not every frame while dragging) - if (isNowActive && !isPreviouslyActive && !undoPushedForSession[label]) { + // Push undo state when slider becomes active + if (s_int8Tracker.UpdateActiveState(label, isNowActive, currentTime, debounceDelay)) { if (g_currentWidget) { EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); - undoPushedForSession[label] = true; } } - // Reset undo flag when slider is completely released and idle for a while - if (!isNowActive && undoPushedForSession[label]) { - if (lastChangeTime.find(label) == lastChangeTime.end() || - ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { - undoPushedForSession[label] = false; - } - } - - // Update active state for next frame - wasActive[label] = isNowActive; - if (changed) { - pendingValues[label] = property; - lastChangeTime[label] = ImGui::GetTime(); + s_int8Tracker.OnValueChanged(label, property, currentTime); } - // Check for any pending values that should be tracked - std::vector toTrack; - for (const auto& [key, changeTime] : lastChangeTime) { - if (ImGui::GetTime() - changeTime >= debounceDelay) { - toTrack.push_back(key); - } - } - - // Track and remove completed entries - for (const auto& key : toTrack) { - PaletteWindow::GetSingleton()->TrackValueUsage(key, static_cast(pendingValues[key])); - pendingValues.erase(key); - lastChangeTime.erase(key); + // Track completed values to palette + auto completed = s_int8Tracker.GetCompletedEntries(currentTime, debounceDelay); + for (const auto& [key, value] : completed) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, static_cast(value)); } return changed; @@ -283,60 +257,28 @@ namespace WeatherUtils bool DrawSliderFloat(const std::string& label, float& property, float min, float max, Widget* widget) { - static std::map pendingValues; - static std::map lastChangeTime; - static std::map wasActive; - static std::map undoPushedForSession; const double debounceDelay = 2.0; - - // Check if item was active in previous frame - bool isPreviouslyActive = wasActive[label]; + double currentTime = ImGui::GetTime(); bool changed = ImGui::SliderFloat(label.c_str(), &property, min, max); - - // Check if item is now active bool isNowActive = ImGui::IsItemActive(); - // Push undo state only once when slider becomes active (not every frame while dragging) - if (isNowActive && !isPreviouslyActive && !undoPushedForSession[label]) { - // Use parameter if provided, otherwise use global widget + // Push undo state when slider becomes active + if (s_floatTracker.UpdateActiveState(label, isNowActive, currentTime, debounceDelay)) { Widget* w = widget ? widget : g_currentWidget; if (w) { EditorWindow::GetSingleton()->PushUndoState(w); - undoPushedForSession[label] = true; } } - // Reset undo flag when slider is completely released and idle for a while - if (!isNowActive && undoPushedForSession[label]) { - // Allow new undo push after slider has been released - if (lastChangeTime.find(label) == lastChangeTime.end() || - ImGui::GetTime() - lastChangeTime[label] >= debounceDelay) { - undoPushedForSession[label] = false; - } - } - - // Update active state for next frame - wasActive[label] = isNowActive; - if (changed) { - pendingValues[label] = property; - lastChangeTime[label] = ImGui::GetTime(); + s_floatTracker.OnValueChanged(label, property, currentTime); } - // Check for any pending values that should be tracked - std::vector toTrack; - for (const auto& [key, changeTime] : lastChangeTime) { - if (ImGui::GetTime() - changeTime >= debounceDelay) { - toTrack.push_back(key); - } - } - - // Track and remove completed entries - for (const auto& key : toTrack) { - PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingValues[key]); - pendingValues.erase(key); - lastChangeTime.erase(key); + // Track completed values to palette + auto completed = s_floatTracker.GetCompletedEntries(currentTime, debounceDelay); + for (const auto& [key, value] : completed) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, value); } return changed; @@ -453,11 +395,13 @@ namespace TOD } } + // Static debounced tracker for TOD slider rows + static DebouncedTracker s_todSliderTracker; + bool DrawTODSliderRow(const char* label, float values[4], float minValue, float maxValue, const char* format) { - static std::map pendingValues; - static std::map lastChangeTime; const double debounceDelay = 2.0; + double currentTime = ImGui::GetTime(); float factors[4]; GetTimeOfDayFactors(factors); @@ -481,11 +425,11 @@ namespace TOD ImGui::PushItemWidth(sliderWidth); std::string id = std::string("##") + label + std::to_string(i); + std::string valueName = std::string(label) + " " + GetPeriodName(i); + if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { changed = true; - std::string valueName = std::string(label) + " " + GetPeriodName(i); - pendingValues[valueName] = values[i]; - lastChangeTime[valueName] = ImGui::GetTime(); + s_todSliderTracker.OnValueChanged(valueName, values[i], currentTime); } if (ImGui::IsItemHovered()) @@ -496,19 +440,9 @@ namespace TOD ImGui::PopStyleVar(); } - // Check for any pending values that should be tracked - std::vector toTrack; - for (const auto& [key, changeTime] : lastChangeTime) { - if (ImGui::GetTime() - changeTime >= debounceDelay) { - toTrack.push_back(key); - } - } - - // Track and remove completed entries - for (const auto& key : toTrack) { - PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingValues[key]); - pendingValues.erase(key); - lastChangeTime.erase(key); + // Track completed entries to palette + for (const auto& [key, value] : s_todSliderTracker.GetCompletedEntries(currentTime, debounceDelay)) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, value); } return changed; @@ -639,13 +573,13 @@ namespace TOD return changed; } + // Static debounced tracker for TOD slider rows with inheritance + static DebouncedTracker s_todSliderInheritTracker; + bool DrawTODSliderRow(const char* label, float values[4], bool inheritFlags[4], const float parentValues[4], float minValue, float maxValue, const char* format) { - static std::map pendingSliderValues; - static std::map sliderLastChangeTime; - static std::map wasActiveInherit; - static std::map undoPushedInherit; const double debounceDelay = 2.0; + double currentTime = ImGui::GetTime(); float factors[4]; GetTimeOfDayFactors(factors); @@ -696,7 +630,6 @@ namespace TOD ImGui::PushItemWidth(sliderWidth); std::string id = std::string("##") + label + std::to_string(i); std::string itemKey = std::string(label) + "_slider_" + std::to_string(i); - bool isPreviouslyActive = wasActiveInherit[itemKey]; ImGui::BeginDisabled(inheritFlags && inheritFlags[i]); if (ImGui::SliderFloat(id.c_str(), &values[i], minValue, maxValue, format)) { @@ -704,26 +637,17 @@ namespace TOD if (inheritFlags) inheritFlags[i] = false; std::string valueName = std::string(label) + " " + GetPeriodName(i); - pendingSliderValues[valueName] = values[i]; - sliderLastChangeTime[valueName] = ImGui::GetTime(); + s_todSliderInheritTracker.OnValueChanged(valueName, values[i], currentTime); } - // Push undo state only once when slider becomes active + // Push undo state when slider becomes active bool isNowActive = ImGui::IsItemActive(); - if (isNowActive && !isPreviouslyActive && !undoPushedInherit[itemKey]) { + if (s_todSliderInheritTracker.UpdateActiveState(itemKey, isNowActive, currentTime, debounceDelay)) { if (g_currentWidget) { EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); - undoPushedInherit[itemKey] = true; } } - // Reset undo flag when slider is released - if (!isNowActive && undoPushedInherit[itemKey]) { - undoPushedInherit[itemKey] = false; - } - - wasActiveInherit[itemKey] = isNowActive; - ImGui::EndDisabled(); if (ImGui::IsItemHovered()) @@ -736,19 +660,9 @@ namespace TOD ImGui::EndGroup(); } - // Check for any pending values that should be tracked - std::vector toTrack; - for (const auto& [key, changeTime] : sliderLastChangeTime) { - if (ImGui::GetTime() - changeTime >= debounceDelay) { - toTrack.push_back(key); - } - } - - // Track and remove completed entries - for (const auto& key : toTrack) { - PaletteWindow::GetSingleton()->TrackValueUsage(key, pendingSliderValues[key]); - pendingSliderValues.erase(key); - sliderLastChangeTime.erase(key); + // Track completed entries to palette + for (const auto& [key, value] : s_todSliderInheritTracker.GetCompletedEntries(currentTime, debounceDelay)) { + PaletteWindow::GetSingleton()->TrackValueUsage(key, value); } return changed; @@ -906,8 +820,14 @@ namespace TOD return changed; } + // Static debounced tracker for TOD float rows + static DebouncedTracker s_todFloatTracker; + bool DrawTODFloatRow(const char* label, float values[4], float minValue, float maxValue, const char* format) { + const double debounceDelay = 2.0; + double currentTime = ImGui::GetTime(); + float factors[4]; GetTimeOfDayFactors(factors); bool changed = false; @@ -921,38 +841,26 @@ namespace TOD float spacing = ImGui::GetStyle().ItemSpacing.x; float columnWidth = (totalWidth - 3 * spacing) / 4.0f; - static std::map wasActiveMap; - static std::map undoPushedMap; - for (int i = 0; i < Count; ++i) { if (i > 0) ImGui::SameLine(); ImGui::PushID(i); std::string itemId = std::string(label) + "_" + std::to_string(i); - bool isPreviouslyActive = wasActiveMap[itemId]; ImGui::SetNextItemWidth(columnWidth); if (ImGui::SliderFloat("##value", &values[i], minValue, maxValue, format)) { changed = true; } - // Push undo state only once when slider becomes active + // Push undo state when slider becomes active bool isNowActive = ImGui::IsItemActive(); - if (isNowActive && !isPreviouslyActive && !undoPushedMap[itemId]) { + if (s_todFloatTracker.UpdateActiveState(itemId, isNowActive, currentTime, debounceDelay)) { if (g_currentWidget) { EditorWindow::GetSingleton()->PushUndoState(g_currentWidget); - undoPushedMap[itemId] = true; } } - // Reset undo flag when slider is released - if (!isNowActive && undoPushedMap[itemId]) { - undoPushedMap[itemId] = false; - } - - wasActiveMap[itemId] = isNowActive; - ImGui::PopID(); } @@ -1076,6 +984,15 @@ namespace TOD { ImGui::EndTable(); } + + void DrawTODSeparator() + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + } } bool BeginWidgetSearchBar(char* searchBuffer, size_t bufferSize, bool& searchActive) @@ -1113,4 +1030,101 @@ bool BeginWidgetSearchBar(char* searchBuffer, size_t bufferSize, bool& searchAct void EndWidgetSearchBar() { // Currently no cleanup needed, but keeping for symmetry and future use -} \ No newline at end of file +} + +// ============================================================================ +// PropertyDrawer Implementation - Consolidates repeated table property drawing +// ============================================================================ +namespace PropertyDrawer +{ + bool BeginTable(const char* tableId, float labelWidth) + { + if (ImGui::BeginTable(tableId, 2, ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("Parameter", ImGuiTableColumnFlags_WidthFixed, labelWidth); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + return true; + } + return false; + } + + void EndTable() + { + ImGui::EndTable(); + } + + void DrawSeparator() + { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Separator(); + ImGui::TableSetColumnIndex(1); + ImGui::Separator(); + } + + bool MatchesSearch(const char* label, const char* searchBuffer) + { + if (!searchBuffer || searchBuffer[0] == '\0') + return true; + return ContainsStringIgnoreCase(label, searchBuffer); + } + + bool DrawFloat(const char* label, float& value, float minVal, float maxVal, + const char* searchBuffer, const char* format) + { + if (!MatchesSearch(label, searchBuffer)) + return false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + + std::string id = std::string("##") + label; + return ImGui::SliderFloat(id.c_str(), &value, minVal, maxVal, format); + } + + bool DrawInt(const char* label, int& value, int minVal, int maxVal, const char* searchBuffer) + { + if (!MatchesSearch(label, searchBuffer)) + return false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + + std::string id = std::string("##") + label; + return ImGui::SliderInt(id.c_str(), &value, minVal, maxVal); + } + + bool DrawColor(const char* label, float3& value, const char* searchBuffer) + { + if (!MatchesSearch(label, searchBuffer)) + return false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + ImGui::SetNextItemWidth(-1); + + return WeatherUtils::DrawColorEdit(label, value); + } + + bool DrawCheckbox(const char* label, bool& value, const char* searchBuffer) + { + if (!MatchesSearch(label, searchBuffer)) + return false; + + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", label); + ImGui::TableSetColumnIndex(1); + + std::string id = std::string("##") + label; + return ImGui::Checkbox(id.c_str(), &value); + } +} // namespace PropertyDrawer +} // namespace PropertyDrawer \ No newline at end of file diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h index c9883aba4a..0efd4e3e33 100644 --- a/src/WeatherEditor/WeatherUtils.h +++ b/src/WeatherEditor/WeatherUtils.h @@ -3,13 +3,188 @@ #include "Util.h" #include #include +#include +#include +#include // Forward declarations class Widget; +class EditorWindow; // Case-insensitive substring search helper bool ContainsStringIgnoreCase(const std::string_view a_string, const std::string_view a_substring); +// ============================================================================ +// DebouncedTracker - Consolidates debounced value tracking with undo support +// ============================================================================ +template +class DebouncedTracker +{ +public: + static constexpr double DefaultDebounceDelay = 2.0; + + // Call when a value changes. Returns true if this is a new interaction session. + bool OnValueChanged(const std::string& key, const T& value, double currentTime) + { + pendingValues[key] = value; + lastChangeTime[key] = currentTime; + return true; + } + + // Call every frame with current item active state. Returns true if undo should be pushed. + bool UpdateActiveState(const std::string& key, bool isNowActive, double currentTime, double debounceDelay = DefaultDebounceDelay) + { + bool isPreviouslyActive = wasActive[key]; + wasActive[key] = isNowActive; + + // Push undo state only once when slider becomes active + if (isNowActive && !isPreviouslyActive && !undoPushedForSession[key]) { + undoPushedForSession[key] = true; + return true; // Signal to push undo + } + + // Reset undo flag when slider is released and idle + if (!isNowActive && undoPushedForSession[key]) { + auto it = lastChangeTime.find(key); + if (it == lastChangeTime.end() || currentTime - it->second >= debounceDelay) { + undoPushedForSession[key] = false; + } + } + + return false; + } + + // Get pending values that have been idle for debounceDelay and should be tracked + std::vector> GetCompletedEntries(double currentTime, double debounceDelay = DefaultDebounceDelay) + { + std::vector> completed; + std::vector keysToRemove; + + for (const auto& [key, changeTime] : lastChangeTime) { + if (currentTime - changeTime >= debounceDelay) { + auto it = pendingValues.find(key); + if (it != pendingValues.end()) { + completed.emplace_back(key, it->second); + keysToRemove.push_back(key); + } + } + } + + for (const auto& key : keysToRemove) { + pendingValues.erase(key); + lastChangeTime.erase(key); + } + + return completed; + } + + void Clear() + { + pendingValues.clear(); + lastChangeTime.clear(); + wasActive.clear(); + undoPushedForSession.clear(); + } + +private: + std::map pendingValues; + std::map lastChangeTime; + std::map wasActive; + std::map undoPushedForSession; +}; + +// ============================================================================ +// PropertyDrawer - Unified table-based property drawing with search support +// ============================================================================ +namespace PropertyDrawer +{ + // Begin a property table. Call before drawing properties. + bool BeginTable(const char* tableId, float labelWidth = 200.0f); + void EndTable(); + + // Draw a table separator row + void DrawSeparator(); + + // Draw properties with optional search filtering. + // searchBuffer can be nullptr to skip search filtering. + // Returns true if value was changed. + bool DrawFloat(const char* label, float& value, float minVal, float maxVal, + const char* searchBuffer = nullptr, const char* format = "%.3f"); + + bool DrawInt(const char* label, int& value, int minVal, int maxVal, + const char* searchBuffer = nullptr); + + bool DrawColor(const char* label, float3& value, const char* searchBuffer = nullptr); + + bool DrawCheckbox(const char* label, bool& value, const char* searchBuffer = nullptr); + + // Check if a label matches the current search (convenience wrapper) + bool MatchesSearch(const char* label, const char* searchBuffer); +} // namespace PropertyDrawer + +// ============================================================================ +// WidgetFactory - Template-based widget creation for EditorWindow::SetupResources +// ============================================================================ +namespace WidgetFactory +{ + // Populate a widget container from a form array + // WidgetType must have a constructor taking FormType* + template + void PopulateWidgets(std::vector>& widgets) + { + auto dataHandler = RE::TESDataHandler::GetSingleton(); + if (!dataHandler) + return; + + auto& formArray = dataHandler->GetFormArray(); + widgets.reserve(widgets.size() + formArray.size()); + + for (auto form : formArray) { + if (form) { + auto widget = std::make_unique(form); + widget->CacheFormData(); + widget->Load(); + widgets.push_back(std::move(widget)); + } + } + } + + // Populate a widget container with SimpleFormWidget for cache-only purposes + template + void PopulateSimpleWidgets(std::vector>& widgets) + { + auto dataHandler = RE::TESDataHandler::GetSingleton(); + if (!dataHandler) + return; + + auto& formArray = dataHandler->GetFormArray(); + widgets.reserve(widgets.size() + formArray.size()); + + for (auto form : formArray) { + if (form) { + auto widget = std::make_unique(); + widget->form = form; + widget->CacheFormData(); + widgets.push_back(std::move(widget)); + } + } + } + + // Draw all open widgets from a container, tracking focus + template + void DrawOpenWidgets(Container& widgets, Widget*& lastFocusedWidget) + { + for (auto& widget : widgets) { + if (widget->IsOpen()) { + widget->DrawWidget(); + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { + lastFocusedWidget = widget.get(); + } + } + } + } +} // namespace WidgetFactory + void Float3ToColor(const float3& newColor, RE::Color& color); void Float3ToColor(const float3& newColor, RE::TESWeather::Data::Color3& color); @@ -78,6 +253,9 @@ namespace TOD // End the TOD table void EndTODTable(); + + // Draw a separator row in a TOD table + void DrawTODSeparator(); } // namespace TOD // Widget search bar helpers From 176c7612ec080e4c4f7c4d427c11e49addb48d8f Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Mon, 29 Dec 2025 18:04:12 -0800 Subject: [PATCH 28/30] fix: alandtse comments --- src/Features/CloudShadows.h | 2 +- src/Features/SkySync.h | 2 +- src/Features/WeatherEditor.h | 2 +- src/Features/WeatherPicker.cpp | 14 -------------- src/Features/WeatherPicker.h | 2 +- src/Menu/FeatureListRenderer.cpp | 2 +- src/State.cpp | 8 +++++--- src/Utils/UI.cpp | 2 +- src/WeatherEditor/WeatherUtils.cpp | 1 - 9 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/Features/CloudShadows.h b/src/Features/CloudShadows.h index b97a726bb0..08c48cebcd 100644 --- a/src/Features/CloudShadows.h +++ b/src/Features/CloudShadows.h @@ -17,7 +17,7 @@ struct CloudShadows : Feature virtual inline std::string GetName() override { return "Cloud Shadows"; } virtual inline std::string GetShortName() override { return "CloudShadows"; } virtual inline std::string GetFeatureModLink() override { return MakeNexusModURL(MOD_ID); } - virtual std::string_view GetCategory() const override { return "Sky & Weather"; } + virtual std::string_view GetCategory() const override { return "Sky"; } virtual inline std::string_view GetShaderDefineName() override { return "CLOUD_SHADOWS"; } virtual std::pair> GetFeatureSummary() override { diff --git a/src/Features/SkySync.h b/src/Features/SkySync.h index f4b99e0313..90546865d6 100644 --- a/src/Features/SkySync.h +++ b/src/Features/SkySync.h @@ -9,7 +9,7 @@ struct SkySync : Feature public: virtual inline std::string GetName() override { return "Sky Sync"; } virtual inline std::string GetShortName() override { return "SkySync"; } - virtual std::string_view GetCategory() const override { return "Sky & Weather"; } + virtual std::string_view GetCategory() const override { return "Sky"; } virtual std::pair> GetFeatureSummary() override { diff --git a/src/Features/WeatherEditor.h b/src/Features/WeatherEditor.h index 53a1e5c095..c905d55ed8 100644 --- a/src/Features/WeatherEditor.h +++ b/src/Features/WeatherEditor.h @@ -16,7 +16,7 @@ struct WeatherEditor : Feature virtual inline std::string GetName() override { return "Weather Editor"; } virtual inline std::string GetShortName() override { return "WeatherEditor"; } virtual inline std::string_view GetShaderDefineName() override { return "WEATHER"; } - virtual inline std::string_view GetCategory() const override { return "Sky & Weather"; } + virtual inline std::string_view GetCategory() const override { return "Sky"; } virtual inline std::pair> GetFeatureSummary() override { return { diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 4ea63685eb..3089a01d19 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -507,20 +507,6 @@ void WeatherPicker::RenderWeatherControls(RE::Sky* sky) ImGui::EndCombo(); } - // Instantly Change Weather button - if (ImGui::Button("Instantly Change Weather", ImVec2(-1, 0))) { - if (s_selectedWeatherIdx >= 0 && s_selectedWeatherIdx < s_filteredWeathers.size()) { - auto selectedWeather = s_filteredWeathers[s_selectedWeatherIdx]; - sky->ForceWeather(selectedWeather, false); - logger::info("[WeatherPicker] Instantly forced weather to: {}", Util::FormatWeather(selectedWeather)); - } - } - if (auto _tt = Util::HoverTooltipWrapper()) { - Util::DrawMultiLineTooltip({ "Immediately forces the selected weather without transition.", - "This bypasses the normal transition system for instant testing." }); - } - - ImGui::Spacing(); } void WeatherPicker::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) diff --git a/src/Features/WeatherPicker.h b/src/Features/WeatherPicker.h index 7a7ca03624..d2601163d8 100644 --- a/src/Features/WeatherPicker.h +++ b/src/Features/WeatherPicker.h @@ -11,7 +11,7 @@ struct WeatherPicker : OverlayFeature virtual bool SupportsVR() override { return true; } virtual bool IsCore() const override { return true; } - virtual std::string_view GetCategory() const override { return "Sky & Weather"; } + virtual std::string_view GetCategory() const override { return "Sky"; } virtual bool IsInMenu() const override { return true; } // Show in main menu to provide weather debugging UI virtual std::pair> GetFeatureSummary() override; diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index c26235c011..0c460dce2f 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -118,7 +118,7 @@ std::vector FeatureListRenderer::BuildMenuLis } // Define category order - std::vector categoryOrder = { "Display", "Debug", "Characters", "Grass", "Lighting", "Materials", "Post-Processing", "Sky & Weather", "Landscape & Textures", "Water", "Other" }; + std::vector categoryOrder = { "Display", "Debug", "Characters", "Grass", "Lighting", "Materials", "Post-Processing", "Sky", "Landscape & Textures", "Water", "Other" }; // Add categorized features to menu with collapsible headers for (const std::string& category : categoryOrder) { if (categorizedFeatures.find(category) != categorizedFeatures.end() && !categorizedFeatures[category].empty()) { diff --git a/src/State.cpp b/src/State.cpp index 8105988067..d104c520aa 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -11,6 +11,7 @@ #include "Features/TerrainBlending.h" #include "Features/TerrainHelper.h" #include "Features/Upscaling.h" +#include "Features/WeatherEditor.h" #include "Menu.h" #include "SettingsOverrideManager.h" #include "ShaderCache.h" @@ -25,13 +26,14 @@ void State::Draw() auto& terrainBlending = globals::features::terrainBlending; auto& terrainHelper = globals::features::terrainHelper; auto& cloudShadows = globals::features::cloudShadows; + auto& weatherEditor = globals::features::weatherEditor; auto truePBR = globals::truePBR; auto context = globals::d3d::context; - // Update weather-based feature settings - WeatherManager::GetSingleton()->UpdateFeatures(); - if (shaderCache->IsEnabled()) { + if (weatherEditor.loaded) + WeatherManager::GetSingleton()->UpdateFeatures(); + if (terrainBlending.loaded) terrainBlending.TerrainShaderHacks(); diff --git a/src/Utils/UI.cpp b/src/Utils/UI.cpp index 952619e0dc..f2524b9f5f 100644 --- a/src/Utils/UI.cpp +++ b/src/Utils/UI.cpp @@ -242,7 +242,7 @@ namespace Util categoryIcon = menu.grass.texture; } else if (strcmp(categoryName, "Lighting") == 0) { categoryIcon = menu.lighting.texture; - } else if (strcmp(categoryName, "Sky & Weather") == 0) { + } else if (strcmp(categoryName, "Sky") == 0) { categoryIcon = menu.sky.texture; } else if (strcmp(categoryName, "Landscape & Textures") == 0) { categoryIcon = menu.landscape.texture; diff --git a/src/WeatherEditor/WeatherUtils.cpp b/src/WeatherEditor/WeatherUtils.cpp index efde65275e..8f0b30c98f 100644 --- a/src/WeatherEditor/WeatherUtils.cpp +++ b/src/WeatherEditor/WeatherUtils.cpp @@ -1126,5 +1126,4 @@ namespace PropertyDrawer std::string id = std::string("##") + label; return ImGui::Checkbox(id.c_str(), &value); } -} // namespace PropertyDrawer } // namespace PropertyDrawer \ No newline at end of file From e95a8a19b32d52361890bcb5065b19eeefcb119d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Dec 2025 02:05:17 +0000 Subject: [PATCH 29/30] =?UTF-8?q?style:=20=F0=9F=8E=A8=20apply=20pre-commi?= =?UTF-8?q?t.ci=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated formatting by clang-format, prettier, and other hooks. See https://pre-commit.ci for details. --- src/Features/WeatherPicker.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Features/WeatherPicker.cpp b/src/Features/WeatherPicker.cpp index 3089a01d19..b5750c41fd 100644 --- a/src/Features/WeatherPicker.cpp +++ b/src/Features/WeatherPicker.cpp @@ -506,7 +506,6 @@ void WeatherPicker::RenderWeatherControls(RE::Sky* sky) } ImGui::EndCombo(); } - } void WeatherPicker::RenderWeatherInformationDisplay(RE::Sky* sky, bool showInteractiveElements) From 4a7d38fb4394de750c53ef28ac30eb2f3e122f6a Mon Sep 17 00:00:00 2001 From: David Kehoe Date: Sun, 4 Jan 2026 00:06:59 -0500 Subject: [PATCH 30/30] fix: widget declaration for build failure --- src/WeatherEditor/WeatherUtils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WeatherEditor/WeatherUtils.h b/src/WeatherEditor/WeatherUtils.h index 0efd4e3e33..30f8524544 100644 --- a/src/WeatherEditor/WeatherUtils.h +++ b/src/WeatherEditor/WeatherUtils.h @@ -1,6 +1,7 @@ #pragma once #include "Util.h" +#include "Widget.h" #include #include #include @@ -8,7 +9,6 @@ #include // Forward declarations -class Widget; class EditorWindow; // Case-insensitive substring search helper