mirror of
				https://github.com/kevinporetti/UnrealImGui.git
				synced 2025-10-30 21:43:17 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									7e756c7cb5
								
							
						
					
					
						commit
						1a6aa98f51
					
				| @ -4,68 +4,141 @@ | ||||
| 
 | ||||
| #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 | ||||
| 	// 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 " | ||||
| 		"was resolved to the default context index %d, which in editor is reserved for editor context. PIE worlds " | ||||
| 		"should use values that start from 1. WorldType = %d, NetMode = %d"), *World.GetName(), | ||||
| 		Utilities::DEFAULT_CONTEXT_INDEX, World.WorldType, World.GetNetMode()); | ||||
| 
 | ||||
| 	// Name for editor ImGui context.
 | ||||
| 	FORCEINLINE FString GetEditorContextName() | ||||
| 	{ | ||||
| 		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
 | ||||
| } | ||||
| 
 | ||||
| 	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) | ||||
| { | ||||
| 	FContextData* Data = Contexts.Find(1); | ||||
| 	if (!Data || !Data->World.IsValid()) | ||||
| 	// In editor, worlds can get invalid. We could remove corresponding entries, but that would mean resetting ImGui
 | ||||
| 	// context every time when PIE session is restarted. Instead we freeze contexts until their worlds are re-created.
 | ||||
| 
 | ||||
| 	for (auto& Pair : Contexts) | ||||
| 	{ | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	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); | ||||
| 		auto& ContextData = Pair.Value; | ||||
| 		if (ContextData.CanTick()) | ||||
| 		{ | ||||
| 			ContextData.ContextProxy.SetAsCurrent(); | ||||
| 			ContextData.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); | ||||
| 		if (!Data->ContextProxy.OnDraw().IsBoundToObject(&ImGuiDemo)) | ||||
| 		{ | ||||
| 			Data->ContextProxy.OnDraw().AddRaw(&ImGuiDemo, &FImGuiDemo::DrawControls); | ||||
| 		} | ||||
| 		Data = &Contexts.Emplace(Utilities::EDITOR_CONTEXT_INDEX, FContextData{ GetEditorContextName(), ImGuiDemo }); | ||||
| 	} | ||||
| 
 | ||||
| 	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; | ||||
| } | ||||
|  | ||||
| @ -4,7 +4,6 @@ | ||||
| 
 | ||||
| #include "ImGuiContextProxy.h" | ||||
| #include "ImGuiDemo.h" | ||||
| #include "Utilities/WorldContextIndex.h" | ||||
| 
 | ||||
| 
 | ||||
| // Manages ImGui context proxies.
 | ||||
| @ -20,17 +19,20 @@ public: | ||||
| 	FImGuiContextManager(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
 | ||||
| 	// context proxy for the only world and the same value as returned from GetWorldContextProxy.
 | ||||
| 	//
 | ||||
| 	// If proxy doesn't exist then it will be created and initialized.
 | ||||
| 	FImGuiContextProxy& GetDefaultContextProxy() { return FindOrAddContextData(Utilities::DEFAULT_CONTEXT_INDEX).ContextProxy; } | ||||
| 	~FImGuiContextManager(); | ||||
| 
 | ||||
| #if WITH_EDITOR | ||||
| 	// Get or create editor ImGui context proxy.
 | ||||
| 	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.
 | ||||
| 	//
 | ||||
| 	// 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); | ||||
| 	FORCEINLINE FImGuiContextProxy& GetWorldContextProxy(const UWorld& World) { return GetWorldContextData(World).ContextProxy; } | ||||
| 
 | ||||
| 	// Get context proxy by index, or null if context with that index doesn't exist.
 | ||||
| 	FORCEINLINE FImGuiContextProxy* GetContextProxy(int32 ContextIndex) | ||||
| @ -43,13 +45,49 @@ public: | ||||
| 
 | ||||
| private: | ||||
| 
 | ||||
| #if WITH_EDITOR | ||||
| 
 | ||||
| 	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; | ||||
| 	}; | ||||
| 
 | ||||
| 	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; | ||||
| 
 | ||||
|  | ||||
| @ -11,7 +11,29 @@ | ||||
| static constexpr float DEFAULT_CANVAS_WIDTH = 3840.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.
 | ||||
| 	Context = ImGui::CreateContext(); | ||||
| @ -22,6 +44,9 @@ FImGuiContextProxy::FImGuiContextProxy() | ||||
| 	// Start initialization.
 | ||||
| 	ImGuiIO& IO = ImGui::GetIO(); | ||||
| 
 | ||||
| 	// Set session data storage.
 | ||||
| 	IO.IniFilename = IniFilename.c_str(); | ||||
| 
 | ||||
| 	// Use pre-defined canvas size.
 | ||||
| 	IO.DisplaySize = { DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT }; | ||||
| 
 | ||||
| @ -53,18 +78,26 @@ FImGuiContextProxy::FImGuiContextProxy(FImGuiContextProxy&& Other) | ||||
| 	, DrawEvent(std::move(Other.DrawEvent)) | ||||
| 	, InputState(std::move(Other.InputState)) | ||||
| 	, DrawLists(std::move(Other.DrawLists)) | ||||
| 	, Name(std::move(Other.Name)) | ||||
| 	, IniFilename(std::move(Other.IniFilename)) | ||||
| { | ||||
| 	Other.Context = nullptr; | ||||
| } | ||||
| 
 | ||||
| FImGuiContextProxy& FImGuiContextProxy::operator=(FImGuiContextProxy&& Other) | ||||
| { | ||||
| 	Context = std::move(Other.Context); | ||||
| 	Other.Context = nullptr; | ||||
| 	// Swapping context so it can be destroyed with the other object.
 | ||||
| 	using std::swap;	 | ||||
| 	swap(Context, Other.Context); | ||||
| 
 | ||||
| 	// Just moving remaining data that doesn't affect cleanup.
 | ||||
| 	bHasActiveItem = Other.bHasActiveItem; | ||||
| 	DrawEvent = std::move(Other.DrawEvent); | ||||
| 	InputState = std::move(Other.InputState); | ||||
| 	DrawLists = std::move(Other.DrawLists); | ||||
| 	Name = std::move(Other.Name); | ||||
| 	IniFilename = std::move(Other.IniFilename); | ||||
| 
 | ||||
| 	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).
 | ||||
| 		SetAsCurrent(); | ||||
| 
 | ||||
| 		// Shutdown to save data etc.
 | ||||
| 		ImGui::Shutdown(); | ||||
| 
 | ||||
| 		// Destroy the context.
 | ||||
| 		// Save context data and destroy.
 | ||||
| 		ImGuiImplementation::SaveCurrentContextIniSettings(IniFilename.c_str()); | ||||
| 		ImGui::DestroyContext(Context); | ||||
| 
 | ||||
| 		// Set default context in ImGui to keep global context pointer valid.
 | ||||
| 		ImGui::SetCurrentContext(&GetDefaultContext()); | ||||
| 		ImGui::SetCurrentContext(&ImGuiImplementation::GetDefaultContext()); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -6,6 +6,8 @@ | ||||
| 
 | ||||
| #include <imgui.h> | ||||
| 
 | ||||
| #include <string> | ||||
| 
 | ||||
| 
 | ||||
| class FImGuiInputState; | ||||
| 
 | ||||
| @ -15,7 +17,7 @@ class FImGuiContextProxy | ||||
| { | ||||
| public: | ||||
| 
 | ||||
| 	FImGuiContextProxy(); | ||||
| 	FImGuiContextProxy(const FString& Name); | ||||
| 	~FImGuiContextProxy(); | ||||
| 
 | ||||
| 	FImGuiContextProxy(const FImGuiContextProxy&) = delete; | ||||
| @ -24,6 +26,9 @@ public: | ||||
| 	FImGuiContextProxy(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.
 | ||||
| 	const TArray<FImGuiDrawList>& GetDrawData() const { return DrawLists; } | ||||
| 
 | ||||
| @ -33,6 +38,9 @@ public: | ||||
| 	// Set input state to be used by this context.
 | ||||
| 	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.
 | ||||
| 	bool IsCurrentContext() const { return ImGui::GetCurrentContext() == Context; } | ||||
| 
 | ||||
| @ -63,4 +71,7 @@ private: | ||||
| 	const FImGuiInputState* InputState = nullptr; | ||||
| 
 | ||||
| 	TArray<FImGuiDrawList> DrawLists; | ||||
| 
 | ||||
| 	FString Name; | ||||
| 	std::string IniFilename; | ||||
| }; | ||||
|  | ||||
| @ -22,9 +22,17 @@ | ||||
| #endif // PLATFORM_WINDOWS
 | ||||
| 
 | ||||
| 
 | ||||
| // 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.
 | ||||
| ImGuiContext& GetDefaultContext() | ||||
| namespace ImGuiImplementation | ||||
| { | ||||
| 	return GImDefaultContext; | ||||
| 	// 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.
 | ||||
| 	ImGuiContext& GetDefaultContext() | ||||
| 	{ | ||||
| 		return GImDefaultContext; | ||||
| 	} | ||||
| 
 | ||||
| 	void SaveCurrentContextIniSettings(const char* Filename) | ||||
| 	{ | ||||
| 		SaveIniSettingsToDisk(Filename); | ||||
| 	} | ||||
| } | ||||
| @ -5,5 +5,12 @@ | ||||
| #include <imgui.h> | ||||
| 
 | ||||
| 
 | ||||
| // Get default context created by ImGui framework.
 | ||||
| ImGuiContext& GetDefaultContext(); | ||||
| // Gives access to selected ImGui implementation features.
 | ||||
| namespace ImGuiImplementation | ||||
| { | ||||
| 	// Get default context created by ImGui framework.
 | ||||
| 	ImGuiContext& GetDefaultContext(); | ||||
| 
 | ||||
| 	// Save current context settings.
 | ||||
| 	void SaveCurrentContextIniSettings(const char* Filename); | ||||
| } | ||||
|  | ||||
| @ -5,15 +5,13 @@ | ||||
| #include "ImGuiModuleManager.h" | ||||
| 
 | ||||
| #include "ImGuiInteroperability.h" | ||||
| #include "Utilities/WorldContextIndex.h" | ||||
| 
 | ||||
| #include <imgui.h> | ||||
| 
 | ||||
| 
 | ||||
| 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.
 | ||||
| 	ViewportCreatedHandle = UGameViewportClient::OnViewportCreated().AddRaw(this, &FImGuiModuleManager::OnViewportCreated); | ||||
| 
 | ||||
| @ -37,6 +35,15 @@ FImGuiModuleManager::~FImGuiModuleManager() | ||||
| 		ViewportCreatedHandle.Reset(); | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove still active widgets (important during hot-reloading).
 | ||||
| 	for (auto& Widget : Widgets) | ||||
| 	{ | ||||
| 		if (Widget.IsValid()) | ||||
| 		{ | ||||
| 			Widget.Pin()->Detach(); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Deactivate this manager.
 | ||||
| 	Uninitialize(); | ||||
| } | ||||
| @ -141,24 +148,21 @@ void FImGuiModuleManager::AddWidgetToViewport(UGameViewportClient* GameViewport) | ||||
| 	checkf(GameViewport, TEXT("Null game viewport.")); | ||||
| 	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.
 | ||||
| 	auto& Proxy = ContextManager.GetWorldContextProxy(*GameViewport->GetWorld()); | ||||
| 
 | ||||
| 	// Get widget for this world.
 | ||||
| 	auto& ViewportWidget = ViewportWidgets.FindOrAdd(ContextIndex); | ||||
| 	if (!ViewportWidget.IsValid()) | ||||
| 	TSharedPtr<SImGuiWidget> Widget; | ||||
| 	SAssignNew(Widget, SImGuiWidget).ModuleManager(this).GameViewport(GameViewport) | ||||
| 		.ContextIndex(Utilities::GetWorldContextIndex(GameViewport)); | ||||
| 
 | ||||
| 	if (TWeakPtr<SImGuiWidget>* Slot = Widgets.FindByPredicate([](auto& Widget) { return !Widget.IsValid(); })) | ||||
| 	{ | ||||
| 		SAssignNew(ViewportWidget, SImGuiWidget).ModuleManager(this).ContextIndex(ContextIndex); | ||||
| 		check(ViewportWidget.IsValid()); | ||||
| 		*Slot = Widget; | ||||
| 	} | ||||
| 	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() | ||||
|  | ||||
| @ -61,8 +61,8 @@ private: | ||||
| 	// Manager for textures resources.
 | ||||
| 	FTextureManager TextureManager; | ||||
| 
 | ||||
| 	// Slate widgets that we attach to game viewports.
 | ||||
| 	TMap<int32, TSharedPtr<SImGuiWidget>> ViewportWidgets; | ||||
| 	// Slate widgets that we created.
 | ||||
| 	TArray<TWeakPtr<SImGuiWidget>> Widgets; | ||||
| 
 | ||||
| 	FDelegateHandle TickDelegateHandle; | ||||
| 	FDelegateHandle ViewportCreatedHandle; | ||||
|  | ||||
| @ -47,10 +47,16 @@ BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION | ||||
| void SImGuiWidget::Construct(const FArguments& InArgs) | ||||
| { | ||||
| 	checkf(InArgs._ModuleManager, TEXT("Null Module Manager argument")); | ||||
| 	checkf(InArgs._GameViewport, TEXT("Null Game Viewport argument")); | ||||
| 
 | ||||
| 	ModuleManager = InArgs._ModuleManager; | ||||
| 	GameViewport = InArgs._GameViewport; | ||||
| 	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.
 | ||||
| 	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.
 | ||||
| 	ModuleManager->OnPostImGuiUpdate().AddRaw(this, &SImGuiWidget::OnPostImGuiUpdate); | ||||
| 
 | ||||
| 	// Register self-debug function.
 | ||||
| 	// Bind this widget to its context proxy.
 | ||||
| 	auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex); | ||||
| 	checkf(ContextProxy, TEXT("Missing context during widget construction: ContextIndex = %d"), ContextIndex); | ||||
| 	ContextProxy->OnDraw().AddRaw(this, &SImGuiWidget::OnDebugDraw); | ||||
| 	ContextProxy->SetInputState(&InputState); | ||||
| } | ||||
| END_SLATE_FUNCTION_BUILD_OPTIMIZATION | ||||
| 
 | ||||
| @ -73,27 +80,20 @@ SImGuiWidget::~SImGuiWidget() | ||||
| 	if (auto* ContextProxy = ModuleManager->GetContextManager().GetContextProxy(ContextIndex)) | ||||
| 	{ | ||||
| 		ContextProxy->OnDraw().RemoveAll(this); | ||||
| 		ContextProxy->RemoveInputState(&InputState); | ||||
| 	} | ||||
| 
 | ||||
| 	// Unregister from post-update notifications.
 | ||||
| 	ModuleManager->OnPostImGuiUpdate().RemoveAll(this); | ||||
| } | ||||
| 
 | ||||
| void SImGuiWidget::AttachToViewport(UGameViewportClient* InGameViewport, bool bResetInput) | ||||
| void SImGuiWidget::Detach() | ||||
| { | ||||
| 	checkf(InGameViewport, TEXT("Null InGameViewport")); | ||||
| 	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) | ||||
| 	if (GameViewport.IsValid()) | ||||
| 	{ | ||||
| 		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) | ||||
| @ -284,13 +284,6 @@ bool SImGuiWidget::IgnoreKeyEvent(const FKeyEvent& KeyEvent) const | ||||
| 	return false; | ||||
| } | ||||
| 
 | ||||
| void SImGuiWidget::ResetInputState() | ||||
| { | ||||
| 	bInputEnabled = false; | ||||
| 	SetVisibilityFromInputEnabled(); | ||||
| 	UpdateInputMode(false, false); | ||||
| } | ||||
| 
 | ||||
| void SImGuiWidget::SetVisibilityFromInputEnabled() | ||||
| { | ||||
| 	// 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; | ||||
| 	if (bDebug) | ||||
| 	{ | ||||
| 		ImGui::SetNextWindowSize(ImVec2(380, 340), ImGuiSetCond_Once); | ||||
| 		ImGui::SetNextWindowSize(ImVec2(380, 360), ImGuiSetCond_Once); | ||||
| 		if (ImGui::Begin("ImGui Widget Debug", &bDebug)) | ||||
| 		{ | ||||
| 			ImGui::Columns(2, nullptr, false); | ||||
| 
 | ||||
| 			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()); | ||||
| 
 | ||||
| 			ImGui::Separator(); | ||||
|  | ||||
| @ -19,6 +19,7 @@ public: | ||||
| 	SLATE_BEGIN_ARGS(SImGuiWidget) | ||||
| 	{} | ||||
| 	SLATE_ARGUMENT(FImGuiModuleManager*, ModuleManager) | ||||
| 	SLATE_ARGUMENT(UGameViewportClient*, GameViewport) | ||||
| 	SLATE_ARGUMENT(int32, ContextIndex) | ||||
| 	SLATE_END_ARGS() | ||||
| 
 | ||||
| @ -35,12 +36,8 @@ public: | ||||
| 	// Get the game viewport to which this widget is attached.
 | ||||
| 	const TWeakObjectPtr<UGameViewportClient>& GetGameViewport() const { return GameViewport; } | ||||
| 
 | ||||
| 	// Attach this widget to a target game viewport.
 | ||||
| 	// Widget can be attached to only one viewport at a time but can be reused after its last viewport becomes invalid
 | ||||
| 	// 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); | ||||
| 	// Detach widget from viewport assigned during construction (effectively allowing to dispose this widget). 
 | ||||
| 	void Detach(); | ||||
| 
 | ||||
| 	//----------------------------------------------------------------------------------------------------
 | ||||
| 	// SWidget overrides
 | ||||
| @ -92,8 +89,6 @@ private: | ||||
| 
 | ||||
| 	bool IgnoreKeyEvent(const FKeyEvent& KeyEvent) const; | ||||
| 
 | ||||
| 	void ResetInputState(); | ||||
| 
 | ||||
| 	// Update visibility based on input enabled state.
 | ||||
| 	void SetVisibilityFromInputEnabled(); | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										24
									
								
								Source/ImGui/Private/Utilities/WorldContext.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Source/ImGui/Private/Utilities/WorldContext.cpp
									
									
									
									
									
										Normal 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; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										41
									
								
								Source/ImGui/Private/Utilities/WorldContext.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								Source/ImGui/Private/Utilities/WorldContext.h
									
									
									
									
									
										Normal 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); | ||||
| } | ||||
| @ -2,8 +2,7 @@ | ||||
| 
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <Core.h> | ||||
| #include <Engine.h> | ||||
| #include "Utilities/WorldContext.h" | ||||
| 
 | ||||
| 
 | ||||
| // Utilities mapping worlds to indices that we use to identify ImGui contexts.
 | ||||
| @ -12,51 +11,34 @@ | ||||
| 
 | ||||
| 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.
 | ||||
| 	static constexpr int32 INVALID_CONTEXT_INDEX = -1; | ||||
| 
 | ||||
| 	// Standalone context index.
 | ||||
| 	static constexpr int32 STANDALONE_GAME_CONTEXT_INDEX = 0; | ||||
| 
 | ||||
| #if WITH_EDITOR | ||||
| 
 | ||||
| 	template<typename T> | ||||
| 	FORCEINLINE int32 GetWorldContextIndex(const TWeakObjectPtr<T>& Obj) | ||||
| 	{ | ||||
| 		return Obj.IsValid() ? GetWorldContextIndex(*Obj.Get()) : INVALID_CONTEXT_INDEX; | ||||
| 	} | ||||
| 	// Editor context index.
 | ||||
| 	static constexpr int32 EDITOR_CONTEXT_INDEX = 0; | ||||
| 
 | ||||
| 	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) | ||||
| 	{ | ||||
| 		// 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
 | ||||
| 		// 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
 | ||||
| 		// and 2+ for clients, what maps directly to our index.
 | ||||
| 
 | ||||
| 		return WorldContext.WorldType == EWorldType::PIE ? FMath::Max(WorldContext.PIEInstance, 1) : DEFAULT_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()); | ||||
| 		return WorldContext.WorldType == EWorldType::PIE ? FMath::Max(WorldContext.PIEInstance, 1) : STANDALONE_GAME_CONTEXT_INDEX; | ||||
| 	} | ||||
| 
 | ||||
| #else | ||||
| @ -65,7 +47,7 @@ namespace Utilities | ||||
| 	constexpr int32 GetWorldContextIndex(const T&) | ||||
| 	{ | ||||
| 		// The only option is standalone game with one context.
 | ||||
| 		return DEFAULT_CONTEXT_INDEX; | ||||
| 		return STANDALONE_GAME_CONTEXT_INDEX; | ||||
| 	} | ||||
| 
 | ||||
| #endif // #if WITH_EDITOR
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Sebastian
						Sebastian