Added support for session reloading and updated contexts and widgets management:

- Added to ImGui Context Proxy a name that is mapped to ini file set in ImGui context.
- ImGui Context Manager generates unique context names from world type and context index.
- Refactored ImGui Context Manager to have a cleaner separation between editor and non-editor bits.
- Fixed context update rules in ImGui Context Manager.
- Changed widgets management in ImGui Module Manager to allow automatic garbage collection after viewports are closed and manual removal when module is shutting down.
- ImGui Widgets are in full control of communication with context proxies.
- Added basic world context utilities.
- Refactored world context index utilities and replaced ambiguous 'default context index' with 'editor' and 'game' ones.
This commit is contained in:
Sebastian 2017-09-27 21:16:54 +01:00
parent 7e756c7cb5
commit 1a6aa98f51
13 changed files with 359 additions and 150 deletions

View File

@ -4,68 +4,141 @@
#include "ImGuiContextManager.h" #include "ImGuiContextManager.h"
#include "ImGuiImplementation.h"
#include "Utilities/WorldContext.h"
#include "Utilities/WorldContextIndex.h"
FImGuiContextProxy& FImGuiContextManager::GetWorldContextProxy(UWorld& World) #include <imgui.h>
namespace
{ {
const int32 Index = Utilities::GetWorldContextIndex(World);
checkf(Index != Utilities::INVALID_CONTEXT_INDEX, TEXT("Couldn't resolve context index for world %s: WorldType = %d"),
*World.GetName(), World.WorldType);
#if WITH_EDITOR #if WITH_EDITOR
// Make sure that PIE worlds don't try to use editor context.
checkf(!GEngine->IsEditor() || Index != Utilities::DEFAULT_CONTEXT_INDEX, TEXT("Index for world %s " // Name for editor ImGui context.
"was resolved to the default context index %d, which in editor is reserved for editor context. PIE worlds " FORCEINLINE FString GetEditorContextName()
"should use values that start from 1. WorldType = %d, NetMode = %d"), *World.GetName(), {
Utilities::DEFAULT_CONTEXT_INDEX, World.WorldType, World.GetNetMode()); return TEXT("Editor");
}
// Name for world ImGui context.
FORCEINLINE FString GetWorldContextName(const UWorld& World)
{
using namespace Utilities;
const FWorldContext* WorldContext = GetWorldContext(World);
switch (WorldContext->WorldType)
{
case EWorldType::PIE:
return FString::Printf(TEXT("PIEContext%d"), GetWorldContextIndex(*WorldContext));
case EWorldType::Game:
return TEXT("Game");
case EWorldType::Editor:
return TEXT("Editor");
default:
return TEXT("Other");
}
}
#else
FORCEINLINE FString GetWorldContextName()
{
return TEXT("Game");
}
FORCEINLINE FString GetWorldContextName(const UWorld&)
{
return TEXT("Game");
}
#endif // WITH_EDITOR #endif // WITH_EDITOR
FContextData& Data = FindOrAddContextData(Index);
// Track worlds to make sure that different worlds don't try to use the same context in the same time.
if (!Data.World.IsValid())
{
Data.World = &World;
}
else
{
checkf(Data.World == &World, TEXT("Two different worlds, %s and %s, resolved to the same world context index %d."),
*Data.World->GetName(), *World.GetName(), Index);
} }
return Data.ContextProxy; FImGuiContextManager::~FImGuiContextManager()
{
Contexts.Empty();
ImGui::Shutdown();
} }
void FImGuiContextManager::Tick(float DeltaSeconds) void FImGuiContextManager::Tick(float DeltaSeconds)
{ {
FContextData* Data = Contexts.Find(1); // In editor, worlds can get invalid. We could remove corresponding entries, but that would mean resetting ImGui
if (!Data || !Data->World.IsValid()) // context every time when PIE session is restarted. Instead we freeze contexts until their worlds are re-created.
for (auto& Pair : Contexts)
{ {
return; auto& ContextData = Pair.Value;
if (ContextData.CanTick())
{
ContextData.ContextProxy.SetAsCurrent();
ContextData.ContextProxy.Tick(DeltaSeconds);
} }
for (auto& Entry : Contexts)
{
FImGuiContextProxy& ContextProxy = Entry.Value.ContextProxy;
ContextProxy.SetAsCurrent();
// Tick context proxy to end the old frame and starts a new one.
ContextProxy.Tick(DeltaSeconds);
} }
} }
FImGuiContextManager::FContextData& FImGuiContextManager::FindOrAddContextData(int32 Index) #if WITH_EDITOR
FImGuiContextManager::FContextData& FImGuiContextManager::GetEditorContextData()
{ {
FContextData* Data = Contexts.Find(Index); FContextData* Data = Contexts.Find(Utilities::EDITOR_CONTEXT_INDEX);
if (!Data) if (UNLIKELY(!Data))
{ {
Data = &Contexts.Add(Index); Data = &Contexts.Emplace(Utilities::EDITOR_CONTEXT_INDEX, FContextData{ GetEditorContextName(), ImGuiDemo });
if (!Data->ContextProxy.OnDraw().IsBoundToObject(&ImGuiDemo))
{
Data->ContextProxy.OnDraw().AddRaw(&ImGuiDemo, &FImGuiDemo::DrawControls);
}
} }
return *Data; return *Data;
} }
#endif // WITH_EDITOR
#if !WITH_EDITOR
FImGuiContextManager::FContextData& FImGuiContextManager::GetStandaloneWorldContextData()
{
FContextData* Data = Contexts.Find(Utilities::STANDALONE_GAME_CONTEXT_INDEX);
if (UNLIKELY(!Data))
{
Data = &Contexts.Emplace(Utilities::STANDALONE_GAME_CONTEXT_INDEX, FContextData{ GetWorldContextName(), ImGuiDemo });
}
return *Data;
}
#endif // !WITH_EDITOR
FImGuiContextManager::FContextData& FImGuiContextManager::GetWorldContextData(const UWorld& World)
{
using namespace Utilities;
const FWorldContext* WorldContext = GetWorldContext(World);
const int32 Index = GetWorldContextIndex(*WorldContext);
checkf(Index != Utilities::INVALID_CONTEXT_INDEX, TEXT("Couldn't find context index for world %s: WorldType = %d"),
*World.GetName(), World.WorldType);
#if WITH_EDITOR
checkf(!GEngine->IsEditor() || Index != Utilities::EDITOR_CONTEXT_INDEX,
TEXT("Context index %d is reserved for editor and cannot be used for world %s: WorldType = %d, NetMode = %d"),
Index, *World.GetName(), World.WorldType, World.GetNetMode());
#endif
FContextData* Data = Contexts.Find(Index);
#if WITH_EDITOR
if (UNLIKELY(!Data))
{
Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), ImGuiDemo, WorldContext->PIEInstance });
}
else
{
// Because we allow (for the sake of continuity) to map different PIE instances to the same index.
Data->PIEInstance = WorldContext->PIEInstance;
}
#else
if (UNLIKELY(!Data))
{
Data = &Contexts.Emplace(Index, FContextData{ GetWorldContextName(World), ImGuiDemo });
}
#endif
return *Data;
}

View File

@ -4,7 +4,6 @@
#include "ImGuiContextProxy.h" #include "ImGuiContextProxy.h"
#include "ImGuiDemo.h" #include "ImGuiDemo.h"
#include "Utilities/WorldContextIndex.h"
// Manages ImGui context proxies. // Manages ImGui context proxies.
@ -20,17 +19,20 @@ public:
FImGuiContextManager(FImGuiContextManager&&) = delete; FImGuiContextManager(FImGuiContextManager&&) = delete;
FImGuiContextManager& operator=(FImGuiContextManager&&) = delete; FImGuiContextManager& operator=(FImGuiContextManager&&) = delete;
// Get or create default ImGui context proxy. In editor this is the editor context proxy and in standalone game ~FImGuiContextManager();
// context proxy for the only world and the same value as returned from GetWorldContextProxy.
// #if WITH_EDITOR
// If proxy doesn't exist then it will be created and initialized. // Get or create editor ImGui context proxy.
FImGuiContextProxy& GetDefaultContextProxy() { return FindOrAddContextData(Utilities::DEFAULT_CONTEXT_INDEX).ContextProxy; } FORCEINLINE FImGuiContextProxy& GetEditorContextProxy() { return GetEditorContextData().ContextProxy; }
#endif
#if !WITH_EDITOR
// Get or create standalone game ImGui context proxy.
FORCEINLINE FImGuiContextProxy& GetWorldContextProxy() { return GetStandaloneWorldContextData().ContextProxy; }
#endif
// Get or create ImGui context proxy for given world. // Get or create ImGui context proxy for given world.
// FORCEINLINE FImGuiContextProxy& GetWorldContextProxy(const UWorld& World) { return GetWorldContextData(World).ContextProxy; }
// If proxy doesn't yet exist then it will be created and initialized. If proxy already exists then associated
// world data will be updated.
FImGuiContextProxy& GetWorldContextProxy(UWorld& World);
// Get context proxy by index, or null if context with that index doesn't exist. // Get context proxy by index, or null if context with that index doesn't exist.
FORCEINLINE FImGuiContextProxy* GetContextProxy(int32 ContextIndex) FORCEINLINE FImGuiContextProxy* GetContextProxy(int32 ContextIndex)
@ -43,13 +45,49 @@ public:
private: private:
#if WITH_EDITOR
struct FContextData struct FContextData
{ {
TWeakObjectPtr<UWorld> World; FContextData(const FString& ContextName, FImGuiDemo& Demo, int32 InPIEInstance = -1)
: PIEInstance(InPIEInstance)
, ContextProxy(ContextName)
{
ContextProxy.OnDraw().AddRaw(&Demo, &FImGuiDemo::DrawControls);
}
FORCEINLINE bool CanTick() const { return PIEInstance < 0 || GEngine->GetWorldContextFromPIEInstance(PIEInstance); }
int32 PIEInstance = -1;
FImGuiContextProxy ContextProxy; FImGuiContextProxy ContextProxy;
}; };
FContextData& FindOrAddContextData(int32 Index); #else
struct FContextData
{
FContextData(const FString& ContextName, FImGuiDemo& Demo)
: ContextProxy(ContextName)
{
ContextProxy.OnDraw().AddRaw(&Demo, &FImGuiDemo::DrawControls);
}
FORCEINLINE bool CanTick() const { return true; }
FImGuiContextProxy ContextProxy;
};
#endif // WITH_EDITOR
#if WITH_EDITOR
FContextData& GetEditorContextData();
#endif
#if !WITH_EDITOR
FContextData& GetStandaloneWorldContextData();
#endif
FContextData& GetWorldContextData(const UWorld& World);
TMap<int32, FContextData> Contexts; TMap<int32, FContextData> Contexts;

View File

@ -11,7 +11,29 @@
static constexpr float DEFAULT_CANVAS_WIDTH = 3840.f; static constexpr float DEFAULT_CANVAS_WIDTH = 3840.f;
static constexpr float DEFAULT_CANVAS_HEIGHT = 2160.f; static constexpr float DEFAULT_CANVAS_HEIGHT = 2160.f;
FImGuiContextProxy::FImGuiContextProxy()
namespace
{
FString GetSaveDirectory()
{
FString Directory = FPaths::Combine(*FPaths::GameSavedDir(), TEXT("ImGui"));
// Make sure that directory is created.
IPlatformFile::GetPlatformPhysical().CreateDirectory(*Directory);
return Directory;
}
FString GetIniFile(const FString& Name)
{
static FString SaveDirectory = GetSaveDirectory();
return FPaths::Combine(SaveDirectory, Name + TEXT(".ini"));
}
}
FImGuiContextProxy::FImGuiContextProxy(const FString& InName)
: Name(InName)
, IniFilename(TCHAR_TO_ANSI(*GetIniFile(InName)))
{ {
// Create context. // Create context.
Context = ImGui::CreateContext(); Context = ImGui::CreateContext();
@ -22,6 +44,9 @@ FImGuiContextProxy::FImGuiContextProxy()
// Start initialization. // Start initialization.
ImGuiIO& IO = ImGui::GetIO(); ImGuiIO& IO = ImGui::GetIO();
// Set session data storage.
IO.IniFilename = IniFilename.c_str();
// Use pre-defined canvas size. // Use pre-defined canvas size.
IO.DisplaySize = { DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT }; IO.DisplaySize = { DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT };
@ -53,18 +78,26 @@ FImGuiContextProxy::FImGuiContextProxy(FImGuiContextProxy&& Other)
, DrawEvent(std::move(Other.DrawEvent)) , DrawEvent(std::move(Other.DrawEvent))
, InputState(std::move(Other.InputState)) , InputState(std::move(Other.InputState))
, DrawLists(std::move(Other.DrawLists)) , DrawLists(std::move(Other.DrawLists))
, Name(std::move(Other.Name))
, IniFilename(std::move(Other.IniFilename))
{ {
Other.Context = nullptr; Other.Context = nullptr;
} }
FImGuiContextProxy& FImGuiContextProxy::operator=(FImGuiContextProxy&& Other) FImGuiContextProxy& FImGuiContextProxy::operator=(FImGuiContextProxy&& Other)
{ {
Context = std::move(Other.Context); // Swapping context so it can be destroyed with the other object.
Other.Context = nullptr; using std::swap;
swap(Context, Other.Context);
// Just moving remaining data that doesn't affect cleanup.
bHasActiveItem = Other.bHasActiveItem; bHasActiveItem = Other.bHasActiveItem;
DrawEvent = std::move(Other.DrawEvent); DrawEvent = std::move(Other.DrawEvent);
InputState = std::move(Other.InputState); InputState = std::move(Other.InputState);
DrawLists = std::move(Other.DrawLists); DrawLists = std::move(Other.DrawLists);
Name = std::move(Other.Name);
IniFilename = std::move(Other.IniFilename);
return *this; return *this;
} }
@ -75,14 +108,12 @@ FImGuiContextProxy::~FImGuiContextProxy()
// Set this context in ImGui for de-initialization (any de-allocations will be tracked in this context). // Set this context in ImGui for de-initialization (any de-allocations will be tracked in this context).
SetAsCurrent(); SetAsCurrent();
// Shutdown to save data etc. // Save context data and destroy.
ImGui::Shutdown(); ImGuiImplementation::SaveCurrentContextIniSettings(IniFilename.c_str());
// Destroy the context.
ImGui::DestroyContext(Context); ImGui::DestroyContext(Context);
// Set default context in ImGui to keep global context pointer valid. // Set default context in ImGui to keep global context pointer valid.
ImGui::SetCurrentContext(&GetDefaultContext()); ImGui::SetCurrentContext(&ImGuiImplementation::GetDefaultContext());
} }
} }

View File

@ -6,6 +6,8 @@
#include <imgui.h> #include <imgui.h>
#include <string>
class FImGuiInputState; class FImGuiInputState;
@ -15,7 +17,7 @@ class FImGuiContextProxy
{ {
public: public:
FImGuiContextProxy(); FImGuiContextProxy(const FString& Name);
~FImGuiContextProxy(); ~FImGuiContextProxy();
FImGuiContextProxy(const FImGuiContextProxy&) = delete; FImGuiContextProxy(const FImGuiContextProxy&) = delete;
@ -24,6 +26,9 @@ public:
FImGuiContextProxy(FImGuiContextProxy&& Other); FImGuiContextProxy(FImGuiContextProxy&& Other);
FImGuiContextProxy& operator=(FImGuiContextProxy&& Other); FImGuiContextProxy& operator=(FImGuiContextProxy&& Other);
// Get the name of this context.
const FString& GetName() const { return Name; }
// Get draw data from the last frame. // Get draw data from the last frame.
const TArray<FImGuiDrawList>& GetDrawData() const { return DrawLists; } const TArray<FImGuiDrawList>& GetDrawData() const { return DrawLists; }
@ -33,6 +38,9 @@ public:
// Set input state to be used by this context. // Set input state to be used by this context.
void SetInputState(const FImGuiInputState* SourceInputState) { InputState = SourceInputState; } void SetInputState(const FImGuiInputState* SourceInputState) { InputState = SourceInputState; }
// If context is currently using input state to remove then remove that binding.
void RemoveInputState(const FImGuiInputState* InputStateToRemove) { if (InputState == InputStateToRemove) InputState = nullptr; }
// Is this context the current ImGui context. // Is this context the current ImGui context.
bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context; } bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context; }
@ -63,4 +71,7 @@ private:
const FImGuiInputState* InputState = nullptr; const FImGuiInputState* InputState = nullptr;
TArray<FImGuiDrawList> DrawLists; TArray<FImGuiDrawList> DrawLists;
FString Name;
std::string IniFilename;
}; };

View File

@ -22,9 +22,17 @@
#endif // PLATFORM_WINDOWS #endif // PLATFORM_WINDOWS
namespace ImGuiImplementation
{
// This is exposing ImGui default context for the whole module. // This is exposing ImGui default context for the whole module.
// This is assuming that we don't define custom GImGui and therefore have GImDefaultContext defined in imgui.cpp. // This is assuming that we don't define custom GImGui and therefore have GImDefaultContext defined in imgui.cpp.
ImGuiContext& GetDefaultContext() ImGuiContext& GetDefaultContext()
{ {
return GImDefaultContext; return GImDefaultContext;
} }
void SaveCurrentContextIniSettings(const char* Filename)
{
SaveIniSettingsToDisk(Filename);
}
}

View File

@ -5,5 +5,12 @@
#include <imgui.h> #include <imgui.h>
// Gives access to selected ImGui implementation features.
namespace ImGuiImplementation
{
// Get default context created by ImGui framework. // Get default context created by ImGui framework.
ImGuiContext& GetDefaultContext(); ImGuiContext& GetDefaultContext();
// Save current context settings.
void SaveCurrentContextIniSettings(const char* Filename);
}

View File

@ -5,15 +5,13 @@
#include "ImGuiModuleManager.h" #include "ImGuiModuleManager.h"
#include "ImGuiInteroperability.h" #include "ImGuiInteroperability.h"
#include "Utilities/WorldContextIndex.h"
#include <imgui.h> #include <imgui.h>
FImGuiModuleManager::FImGuiModuleManager() FImGuiModuleManager::FImGuiModuleManager()
{ {
// Make sure that default ImGui context is setup.
ContextManager.GetDefaultContextProxy();
// Typically we will use viewport created events to add widget to new game viewports. // Typically we will use viewport created events to add widget to new game viewports.
ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated); ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated);
@ -37,6 +35,15 @@ FImGuiModuleManager::~FImGuiModuleManager()
ViewportCreatedHandle.Reset(); ViewportCreatedHandle.Reset();
} }
// Remove still active widgets (important during hot-reloading).
for (auto& Widget : Widgets)
{
if (Widget.IsValid())
{
Widget.Pin()->Detach();
}
}
// Deactivate this manager. // Deactivate this manager.
Uninitialize(); Uninitialize();
} }
@ -141,24 +148,21 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport)
checkf(GameViewport, TEXT("Null game viewport.")); checkf(GameViewport, TEXT("Null game viewport."));
checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports.")); checkf(FSlateApplication::IsInitialized(), TEXT("Slate should be initialized before we can add widget to game viewports."));
const int32 ContextIndex = Utilities::GetWorldContextIndex(GameViewport);
// This makes sure that context for this world is created. // This makes sure that context for this world is created.
auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld()); auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld());
// Get widget for this world. TSharedPtr<SImGuiWidget> Widget;
auto& ViewportWidget = ViewportWidgets.FindOrAdd(ContextIndex); SAssignNew(Widget, SImGuiWidget).ModuleManager(this).GameViewport(GameViewport)
if (!ViewportWidget.IsValid()) .ContextIndex(Utilities::GetWorldContextIndex(GameViewport));
if (TWeakPtr<SImGuiWidget>* Slot = Widgets.FindByPredicate([](auto& Widget) { return !Widget.IsValid(); }))
{ {
SAssignNew(ViewportWidget, SImGuiWidget).ModuleManager(this).ContextIndex(ContextIndex); *Slot = Widget;
check(ViewportWidget.IsValid()); }
else
{
Widgets.Emplace(Widget);
} }
// Bind widget's input to context for this world.
Proxy.SetInputState(&ViewportWidget->GetInputState());
// We should always have one viewport per context index at a time (this will be validated by widget).
ViewportWidget->AttachToViewport(GameViewport);
} }
void FImGuiModuleManager::AddWidgetToAllViewports() void FImGuiModuleManager::AddWidgetToAllViewports()

View File

@ -61,8 +61,8 @@ private:
// Manager for textures resources. // Manager for textures resources.
FTextureManager TextureManager; FTextureManager TextureManager;
// Slate widgets that we attach to game viewports. // Slate widgets that we created.
TMap<int32, TSharedPtr<SImGuiWidget>> ViewportWidgets; TArray<TWeakPtr<SImGuiWidget>> Widgets;
FDelegateHandle TickDelegateHandle; FDelegateHandle TickDelegateHandle;
FDelegateHandle ViewportCreatedHandle; FDelegateHandle ViewportCreatedHandle;

View File

@ -47,10 +47,16 @@ BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SImGuiWidget::Construct(const FArguments& InArgs) void SImGuiWidget::Construct(const FArguments& InArgs)
{ {
checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument")); checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument"));
checkf(InArgs._GameViewport, TEXT("Null Game Viewport argument"));
ModuleManager = InArgs._ModuleManager; ModuleManager = InArgs._ModuleManager;
GameViewport = InArgs._GameViewport;
ContextIndex = InArgs._ContextIndex; ContextIndex = InArgs._ContextIndex;
// NOTE: We could allow null game viewports (for instance to attach to non-viewport widgets) but we would need
// to modify a few functions that assume valid viewport pointer.
GameViewport->AddViewportWidgetContent(SharedThis(this), IMGUI_WIDGET_Z_ORDER);
// Disable mouse cursor over this widget as we will use ImGui to draw it. // Disable mouse cursor over this widget as we will use ImGui to draw it.
SetCursor(EMouseCursor::None); SetCursor(EMouseCursor::None);
@ -60,10 +66,11 @@ void SImGuiWidget::Construct(const FArguments& InArgs)
// Register to get post-update notifications, so we can clean frame updates. // Register to get post-update notifications, so we can clean frame updates.
ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate); ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate);
// Register self-debug function. // Bind this widget to its context proxy.
auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex);
checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex); checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex);
ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw); ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw);
ContextProxy->SetInputState(&InputState);
} }
END_SLATE_FUNCTION_BUILD_OPTIMIZATION END_SLATE_FUNCTION_BUILD_OPTIMIZATION
@ -73,27 +80,20 @@ SImGuiWidget::~SImGuiWidget()
if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex))
{ {
ContextProxy->OnDraw().RemoveAll(this); ContextProxy->OnDraw().RemoveAll(this);
ContextProxy->RemoveInputState(&InputState);
} }
// Unregister from post-update notifications. // Unregister from post-update notifications.
ModuleManager->OnPostImGuiUpdate().RemoveAll(this); ModuleManager->OnPostImGuiUpdate().RemoveAll(this);
} }
void SImGuiWidget::AttachToViewport(UGameViewportClient* InGameViewport, bool bResetInput) void SImGuiWidget::Detach()
{ {
checkf(InGameViewport, TEXT("Null InGameViewport")); if (GameViewport.IsValid())
checkf(!GameViewport.IsValid() || GameViewport.Get() == InGameViewport,
TEXT("Widget is attached to another game viewport and will be available for reuse only after this session ")
TEXT("ends. ContextIndex = %d, CurrentGameViewport = %s, InGameViewport = %s"),
ContextIndex, *GameViewport->GetName(), InGameViewport->GetName());
if (bResetInput)
{ {
ResetInputState(); GameViewport->RemoveViewportWidgetContent(SharedThis(this));
GameViewport.Reset();
} }
GameViewport = InGameViewport;
GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(SharedThis(this)), IMGUI_WIDGET_Z_ORDER);
} }
void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) void SImGuiWidget::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
@ -284,13 +284,6 @@ bool SImGuiWidget::IgnoreKeyEvent(const FKeyEvent& KeyEvent) const
return false; return false;
} }
void SImGuiWidget::ResetInputState()
{
bInputEnabled = false;
SetVisibilityFromInputEnabled();
UpdateInputMode(false, false);
}
void SImGuiWidget::SetVisibilityFromInputEnabled() void SImGuiWidget::SetVisibilityFromInputEnabled()
{ {
// If we don't use input disable hit test to make this widget invisible for cursors hit detection. // If we don't use input disable hit test to make this widget invisible for cursors hit detection.
@ -479,12 +472,14 @@ void SImGuiWidget::OnDebugDraw()
bool bDebug = CVars::DebugWidget.GetValueOnGameThread() > 0; bool bDebug = CVars::DebugWidget.GetValueOnGameThread() > 0;
if (bDebug) if (bDebug)
{ {
ImGui::SetNextWindowSize(ImVec2(380, 340), ImGuiSetCond_Once); ImGui::SetNextWindowSize(ImVec2(380, 360), ImGuiSetCond_Once);
if (ImGui::Begin("ImGui Widget Debug", &bDebug)) if (ImGui::Begin("ImGui Widget Debug", &bDebug))
{ {
ImGui::Columns(2, nullptr, false); ImGui::Columns(2, nullptr, false);
TwoColumns::Value("Context Index", ContextIndex); TwoColumns::Value("Context Index", ContextIndex);
FImGuiContextProxy* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex);
TwoColumns::Value("Context Name", ContextProxy ? *ContextProxy->GetName() : TEXT("< Null >"));
TwoColumns::Value("Game Viewport", *GameViewport->GetName()); TwoColumns::Value("Game Viewport", *GameViewport->GetName());
ImGui::Separator(); ImGui::Separator();

View File

@ -19,6 +19,7 @@ public:
SLATE_BEGIN_ARGS(SImGuiWidget) SLATE_BEGIN_ARGS(SImGuiWidget)
{} {}
SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager) SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager)
SLATE_ARGUMENT(UGameViewportClient*, GameViewport)
SLATE_ARGUMENT(int32, ContextIndex) SLATE_ARGUMENT(int32, ContextIndex)
SLATE_END_ARGS() SLATE_END_ARGS()
@ -35,12 +36,8 @@ public:
// Get the game viewport to which this widget is attached. // Get the game viewport to which this widget is attached.
const TWeakObjectPtr<UGameViewportClient>& GetGameViewport() const { return GameViewport; } const TWeakObjectPtr<UGameViewportClient>& GetGameViewport() const { return GameViewport; }
// Attach this widget to a target game viewport. // Detach widget from viewport assigned during construction (effectively allowing to dispose this widget).
// Widget can be attached to only one viewport at a time but can be reused after its last viewport becomes invalid void Detach();
// at the end of a session. Widgets are weakly attached, so once destroyed they are automatically removed.
// @param InGameViewport - Target game viewport
// @param bResetInput - If true (default), input will be reset back to a default state
void AttachToViewport(UGameViewportClient* InGameViewport, bool bResetInput = true);
//---------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------------------------------
// SWidget overrides // SWidget overrides
@ -92,8 +89,6 @@ private:
bool IgnoreKeyEvent(const FKeyEvent& KeyEvent) const; bool IgnoreKeyEvent(const FKeyEvent& KeyEvent) const;
void ResetInputState();
// Update visibility based on input enabled state. // Update visibility based on input enabled state.
void SetVisibilityFromInputEnabled(); void SetVisibilityFromInputEnabled();

View File

@ -0,0 +1,24 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#include "ImGuiPrivatePCH.h"
#include "WorldContext.h"
namespace Utilities
{
const FWorldContext* GetWorldContextFromNetMode(ENetMode NetMode)
{
checkf(GEngine, TEXT("GEngine required to get list of worlds."));
for (const FWorldContext& WorldContext : GEngine->GetWorldContexts())
{
if (WorldContext.World() && WorldContext.World()->GetNetMode() == NetMode)
{
return &WorldContext;
}
}
return nullptr;
}
}

View File

@ -0,0 +1,41 @@
// Distributed under the MIT License (MIT) (see accompanying LICENSE file)
#pragma once
#include <Core.h>
#include <Engine.h>
// Utilities helping to get a World Context.
namespace Utilities
{
template<typename T>
FORCEINLINE const FWorldContext* GetWorldContext(const TWeakObjectPtr<T>& Obj)
{
return Obj.IsValid() ? GetWorldContext(*Obj.Get()) : nullptr;
}
template<typename T>
FORCEINLINE const FWorldContext* GetWorldContext(const T* Obj)
{
return Obj ? GetWorldContext(*Obj) : nullptr;
}
FORCEINLINE const FWorldContext* GetWorldContext(const UGameInstance& GameInstance)
{
return GameInstance.GetWorldContext();
}
FORCEINLINE const FWorldContext* GetWorldContext(const UGameViewportClient& GameViewportClient)
{
return GetWorldContext(GameViewportClient.GetGameInstance());
}
FORCEINLINE const FWorldContext* GetWorldContext(const UWorld& World)
{
return GetWorldContext(World.GetGameInstance());
}
const FWorldContext* GetWorldContextFromNetMode(ENetMode NetMode);
}

View File

@ -2,8 +2,7 @@
#pragma once #pragma once
#include <Core.h> #include "Utilities/WorldContext.h"
#include <Engine.h>
// Utilities mapping worlds to indices that we use to identify ImGui contexts. // Utilities mapping worlds to indices that we use to identify ImGui contexts.
@ -12,51 +11,34 @@
namespace Utilities namespace Utilities
{ {
// Default context index for non PIE worlds.
static constexpr int32 DEFAULT_CONTEXT_INDEX = 0;
// Invalid context index for parameters that cannot be resolved to a valid world. // Invalid context index for parameters that cannot be resolved to a valid world.
static constexpr int32 INVALID_CONTEXT_INDEX = -1; static constexpr int32 INVALID_CONTEXT_INDEX = -1;
// Standalone context index.
static constexpr int32 STANDALONE_GAME_CONTEXT_INDEX = 0;
#if WITH_EDITOR #if WITH_EDITOR
template<typename T> // Editor context index.
FORCEINLINE int32 GetWorldContextIndex(const TWeakObjectPtr<T>& Obj) static constexpr int32 EDITOR_CONTEXT_INDEX = 0;
{
return Obj.IsValid() ? GetWorldContextIndex(*Obj.Get()) : INVALID_CONTEXT_INDEX;
}
template<typename T> template<typename T>
FORCEINLINE int32 GetWorldContextIndex(const T* Obj) FORCEINLINE int32 GetWorldContextIndex(const T& Obj)
{ {
return Obj ? GetWorldContextIndex(*Obj) : INVALID_CONTEXT_INDEX; const FWorldContext* WorldContext = GetWorldContext(Obj);
return WorldContext ? GetWorldContextIndex(*WorldContext) : INVALID_CONTEXT_INDEX;
} }
FORCEINLINE int32 GetWorldContextIndex(const FWorldContext& WorldContext) FORCEINLINE int32 GetWorldContextIndex(const FWorldContext& WorldContext)
{ {
// In standalone game (WorldType = Game) we have only one context with index 0 (see DEFAULT_CONTEXT_INDEX). // In standalone game (WorldType = Game) we have only one context with index 0 (see GAME_CONTEXT_INDEX).
// In editor, we keep 0 for editor and use PIEInstance to index worlds. In simulation or standalone single-PIE // In editor, we keep 0 for editor and use PIEInstance to index worlds. In simulation or standalone single-PIE
// sessions PIEInstance is 0, but since there is only one world we can change it without causing any conflicts. // sessions PIEInstance is 0, but since there is only one world we can change it without causing any conflicts.
// In single-PIE with dedicated server or multi-PIE sessions worlds have PIEInstance starting from 1 for server // In single-PIE with dedicated server or multi-PIE sessions worlds have PIEInstance starting from 1 for server
// and 2+ for clients, what maps directly to our index. // and 2+ for clients, what maps directly to our index.
return WorldContext.WorldType == EWorldType::PIE ? FMath::Max(WorldContext.PIEInstance, 1) : DEFAULT_CONTEXT_INDEX; return WorldContext.WorldType == EWorldType::PIE ? FMath::Max(WorldContext.PIEInstance, 1) : STANDALONE_GAME_CONTEXT_INDEX;
}
FORCEINLINE int32 GetWorldContextIndex(const UGameInstance& GameInstance)
{
return GetWorldContextIndex(GameInstance.GetWorldContext());
}
FORCEINLINE int32 GetWorldContextIndex(const UGameViewportClient& GameViewportClient)
{
return GetWorldContextIndex(GameViewportClient.GetGameInstance());
}
FORCEINLINE int32 GetWorldContextIndex(const UWorld& World)
{
return GetWorldContextIndex(World.GetGameInstance());
} }
#else #else
@ -65,7 +47,7 @@ namespace Utilities
constexpr int32 GetWorldContextIndex(const T&) constexpr int32 GetWorldContextIndex(const T&)
{ {
// The only option is standalone game with one context. // The only option is standalone game with one context.
return DEFAULT_CONTEXT_INDEX; return STANDALONE_GAME_CONTEXT_INDEX;
} }
#endif // #if WITH_EDITOR #endif // #if WITH_EDITOR