Add runtime property editing, scene outliner, and spawned-only selection
Property Editing: - New EditableComponent with two-step dropdown (Select Component → Add Property) to declare which properties are exposed in the runtime editor - Dynamic property panel below Transform: supports float, bool, string, FVector, FRotator, FLinearColor with per-tick refresh and keyboard focus guard - PropertyAction undo/redo via ExportText/ImportText reflection - Custom properties serialized to JSON (TMap in ActorData, version 2) - Copy/paste/duplicate propagate custom property values Scene Outliner: - Scrollable list of spawned actors in the right panel - Click to select, selected actor highlighted in blue - Auto-refreshes when actors are added/removed Selection Filter: - Only actors spawned by the editor (catalogue, duplicate, paste, load) are selectable — static scene geometry is no longer clickable Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -3,10 +3,89 @@
|
|||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "GameFramework/Pawn.h"
|
#include "GameFramework/Pawn.h"
|
||||||
#include "Components/PrimitiveComponent.h"
|
#include "Components/PrimitiveComponent.h"
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
|
|
||||||
|
// Helper: export all editable property values from an actor
|
||||||
|
static TMap<FString, FString> ExportEditableProps(AActor* Actor)
|
||||||
|
{
|
||||||
|
TMap<FString, FString> Result;
|
||||||
|
if (!Actor) return Result;
|
||||||
|
|
||||||
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
|
if (!EditComp) return Result;
|
||||||
|
|
||||||
|
for (const FName& Path : EditComp->EditableProperties)
|
||||||
|
{
|
||||||
|
FString CompName, PropName;
|
||||||
|
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||||
|
const FString Key = Path.ToString();
|
||||||
|
|
||||||
|
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Comps;
|
||||||
|
Actor->GetComponents(Comps);
|
||||||
|
for (UActorComponent* Comp : Comps)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == CompName) { Target = Comp; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Target) continue;
|
||||||
|
|
||||||
|
FProperty* Prop = FindFProperty<FProperty>(Target->GetClass(), *PropName);
|
||||||
|
if (!Prop) continue;
|
||||||
|
|
||||||
|
FString ValStr;
|
||||||
|
Prop->ExportText_InContainer(0, ValStr, Target, Target, nullptr, PPF_None);
|
||||||
|
Result.Add(Key, ValStr);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: import custom property values onto an actor
|
||||||
|
static void ImportEditableProps(AActor* Actor, const TMap<FString, FString>& Properties)
|
||||||
|
{
|
||||||
|
if (!Actor || Properties.Num() == 0) return;
|
||||||
|
|
||||||
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
|
if (!EditComp) return;
|
||||||
|
|
||||||
|
for (const FName& Path : EditComp->EditableProperties)
|
||||||
|
{
|
||||||
|
FString CompName, PropName;
|
||||||
|
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||||
|
const FString Key = Path.ToString();
|
||||||
|
|
||||||
|
const FString* ValStr = Properties.Find(Key);
|
||||||
|
if (!ValStr) continue;
|
||||||
|
|
||||||
|
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Comps;
|
||||||
|
Actor->GetComponents(Comps);
|
||||||
|
for (UActorComponent* Comp : Comps)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == CompName) { Target = Comp; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Target) continue;
|
||||||
|
|
||||||
|
FProperty* Prop = FindFProperty<FProperty>(Target->GetClass(), *PropName);
|
||||||
|
if (!Prop) continue;
|
||||||
|
|
||||||
|
Prop->ImportText_InContainer(**ValStr, Target, Target, PPF_None);
|
||||||
|
if (UActorComponent* AsComp = Cast<UActorComponent>(Target))
|
||||||
|
{
|
||||||
|
AsComp->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_SelectionManager::Initialize(APlayerController* InOwnerPC)
|
void UPS_Editor_SelectionManager::Initialize(APlayerController* InOwnerPC)
|
||||||
{
|
{
|
||||||
@@ -53,9 +132,15 @@ void UPS_Editor_SelectionManager::HandleClickAtScreenPosition(FVector2D ScreenPo
|
|||||||
{
|
{
|
||||||
AActor* HitActor = Hit.GetActor();
|
AActor* HitActor = Hit.GetActor();
|
||||||
|
|
||||||
// Only allow selection of Movable actors (skip static scene geometry like floors/walls)
|
// Only allow selection of actors spawned by the editor (catalogue, duplicate, paste, load)
|
||||||
USceneComponent* RootComp = HitActor->GetRootComponent();
|
bool bIsSelectable = false;
|
||||||
const bool bIsSelectable = RootComp && RootComp->Mobility == EComponentMobility::Movable;
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||||
|
{
|
||||||
|
bIsSelectable = SpawnMgr->IsActorSpawned(HitActor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (bIsSelectable)
|
if (bIsSelectable)
|
||||||
{
|
{
|
||||||
@@ -410,6 +495,10 @@ void UPS_Editor_SelectionManager::DuplicateSelection()
|
|||||||
{
|
{
|
||||||
Root->SetMobility(EComponentMobility::Movable);
|
Root->SetMobility(EComponentMobility::Movable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy custom editable properties from original
|
||||||
|
ImportEditableProps(NewActor, ExportEditableProps(Original));
|
||||||
|
|
||||||
NewActors.Add(NewActor);
|
NewActors.Add(NewActor);
|
||||||
|
|
||||||
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||||
@@ -462,6 +551,7 @@ void UPS_Editor_SelectionManager::CopySelection()
|
|||||||
Entry.RelativeLocation = Actor->GetActorLocation() - Pivot;
|
Entry.RelativeLocation = Actor->GetActorLocation() - Pivot;
|
||||||
Entry.Rotation = Actor->GetActorRotation();
|
Entry.Rotation = Actor->GetActorRotation();
|
||||||
Entry.Scale = Actor->GetActorScale3D();
|
Entry.Scale = Actor->GetActorScale3D();
|
||||||
|
Entry.CustomProperties = ExportEditableProps(Actor);
|
||||||
Clipboard.Add(Entry);
|
Clipboard.Add(Entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +596,10 @@ void UPS_Editor_SelectionManager::PasteClipboard()
|
|||||||
{
|
{
|
||||||
Root->SetMobility(EComponentMobility::Movable);
|
Root->SetMobility(EComponentMobility::Movable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore custom editable properties
|
||||||
|
ImportEditableProps(NewActor, Entry.CustomProperties);
|
||||||
|
|
||||||
NewActors.Add(NewActor);
|
NewActors.Add(NewActor);
|
||||||
|
|
||||||
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct FPS_Editor_ClipboardEntry
|
|||||||
FVector RelativeLocation = FVector::ZeroVector; // Relative to clipboard pivot
|
FVector RelativeLocation = FVector::ZeroVector; // Relative to clipboard pivot
|
||||||
FRotator Rotation = FRotator::ZeroRotator;
|
FRotator Rotation = FRotator::ZeroRotator;
|
||||||
FVector Scale = FVector::OneVector;
|
FVector Scale = FVector::OneVector;
|
||||||
|
TMap<FString, FString> CustomProperties; // Editable property values
|
||||||
};
|
};
|
||||||
|
|
||||||
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
||||||
|
|||||||
@@ -1,4 +1,44 @@
|
|||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
|
||||||
|
void FPS_Editor_PropertyAction::ApplyValue(const FString& ValueStr)
|
||||||
|
{
|
||||||
|
AActor* Act = Actor.Get();
|
||||||
|
if (!Act) return;
|
||||||
|
|
||||||
|
UObject* TargetObject = nullptr;
|
||||||
|
if (ComponentName.IsEmpty())
|
||||||
|
{
|
||||||
|
TargetObject = Act;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Components;
|
||||||
|
Act->GetComponents(Components);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == ComponentName)
|
||||||
|
{
|
||||||
|
TargetObject = Comp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetObject) return;
|
||||||
|
|
||||||
|
FProperty* Prop = FindFProperty<FProperty>(TargetObject->GetClass(), *PropertyName);
|
||||||
|
if (!Prop) return;
|
||||||
|
|
||||||
|
Prop->ImportText_InContainer(*ValueStr, TargetObject, TargetObject, PPF_None);
|
||||||
|
|
||||||
|
// Refresh rendering for visual updates
|
||||||
|
if (UActorComponent* Comp = Cast<UActorComponent>(TargetObject))
|
||||||
|
{
|
||||||
|
Comp->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void UPS_Editor_UndoManager::RecordAction(TSharedPtr<FPS_Editor_Action> Action)
|
void UPS_Editor_UndoManager::RecordAction(TSharedPtr<FPS_Editor_Action> Action)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ enum class EPS_Editor_ActionType : uint8
|
|||||||
{
|
{
|
||||||
Transform,
|
Transform,
|
||||||
Delete,
|
Delete,
|
||||||
Spawn
|
Spawn,
|
||||||
|
PropertyChange
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,6 +157,29 @@ struct FPS_Editor_SpawnAction : public FPS_Editor_Action
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a single property value change on a component or actor.
|
||||||
|
* Stores text-serialized old/new values for undo/redo via ImportText.
|
||||||
|
*/
|
||||||
|
struct FPS_Editor_PropertyAction : public FPS_Editor_Action
|
||||||
|
{
|
||||||
|
FPS_Editor_PropertyAction() : FPS_Editor_Action(EPS_Editor_ActionType::PropertyChange) {}
|
||||||
|
|
||||||
|
TWeakObjectPtr<AActor> Actor;
|
||||||
|
FString ComponentName; // Empty = actor property
|
||||||
|
FString PropertyName;
|
||||||
|
FString OldValue; // Text-serialized via ExportText
|
||||||
|
FString NewValue; // Text-serialized via ExportText
|
||||||
|
FString Description;
|
||||||
|
|
||||||
|
virtual void Execute() override { ApplyValue(NewValue); }
|
||||||
|
virtual void Undo() override { ApplyValue(OldValue); }
|
||||||
|
virtual FString GetDescription() const override { return Description; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ApplyValue(const FString& ValueStr);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime undo/redo manager for PS_Editor.
|
* Runtime undo/redo manager for PS_Editor.
|
||||||
* Maintains a linear action stack with a cursor.
|
* Maintains a linear action stack with a cursor.
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ struct FPS_Editor_ActorData
|
|||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FVector Scale = FVector::OneVector;
|
FVector Scale = FVector::OneVector;
|
||||||
|
|
||||||
|
/** Custom property values: key = "ComponentName.PropertyName", value = text-serialized value. */
|
||||||
|
UPROPERTY()
|
||||||
|
TMap<FString, FString> CustomProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Serialized data for a complete scene. */
|
/** Serialized data for a complete scene. */
|
||||||
@@ -30,7 +34,7 @@ struct FPS_Editor_SceneData
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
int32 Version = 1;
|
int32 Version = 2;
|
||||||
|
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FString SceneName;
|
FString SceneName;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_SpawnManager.h"
|
#include "PS_Editor_SpawnManager.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
#include "JsonObjectConverter.h"
|
#include "JsonObjectConverter.h"
|
||||||
#include "Misc/FileHelper.h"
|
#include "Misc/FileHelper.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
#include "HAL/PlatformFileManager.h"
|
#include "HAL/PlatformFileManager.h"
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
|
||||||
bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
|
bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_SpawnManager* SpawnManager)
|
||||||
{
|
{
|
||||||
@@ -33,6 +36,43 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
|||||||
ActorData.Rotation = Actor->GetActorRotation();
|
ActorData.Rotation = Actor->GetActorRotation();
|
||||||
ActorData.Scale = Actor->GetActorScale3D();
|
ActorData.Scale = Actor->GetActorScale3D();
|
||||||
|
|
||||||
|
// Export custom editable properties
|
||||||
|
if (UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||||
|
{
|
||||||
|
for (const FName& Path : EditComp->EditableProperties)
|
||||||
|
{
|
||||||
|
FString CompName, PropName;
|
||||||
|
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||||
|
const FString Key = Path.ToString();
|
||||||
|
|
||||||
|
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(Actor) : nullptr;
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Components;
|
||||||
|
Actor->GetComponents(Components);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == CompName)
|
||||||
|
{
|
||||||
|
Target = Comp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Target)
|
||||||
|
{
|
||||||
|
FProperty* Prop = FindFProperty<FProperty>(Target->GetClass(), *PropName);
|
||||||
|
if (Prop)
|
||||||
|
{
|
||||||
|
FString ValueStr;
|
||||||
|
Prop->ExportText_InContainer(0, ValueStr, Target, Target, nullptr, PPF_None);
|
||||||
|
ActorData.CustomProperties.Add(Key, ValueStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SceneData.Actors.Add(ActorData);
|
SceneData.Actors.Add(ActorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +145,51 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
AActor* SpawnedActor = SpawnManager->SpawnActorDirect(LoadedClass, ActorData.Location, ActorData.Rotation, ActorData.Scale);
|
AActor* SpawnedActor = SpawnManager->SpawnActorDirect(LoadedClass, ActorData.Location, ActorData.Rotation, ActorData.Scale);
|
||||||
if (SpawnedActor)
|
if (SpawnedActor)
|
||||||
{
|
{
|
||||||
|
// Import custom properties
|
||||||
|
if (ActorData.CustomProperties.Num() > 0)
|
||||||
|
{
|
||||||
|
if (UPS_Editor_EditableComponent* EditComp = SpawnedActor->FindComponentByClass<UPS_Editor_EditableComponent>())
|
||||||
|
{
|
||||||
|
for (const FName& Path : EditComp->EditableProperties)
|
||||||
|
{
|
||||||
|
FString CompName, PropName;
|
||||||
|
UPS_Editor_EditableComponent::ParsePropertyPath(Path, CompName, PropName);
|
||||||
|
const FString Key = Path.ToString();
|
||||||
|
|
||||||
|
const FString* ValueStr = ActorData.CustomProperties.Find(Key);
|
||||||
|
if (!ValueStr) continue;
|
||||||
|
|
||||||
|
UObject* Target = CompName.IsEmpty() ? static_cast<UObject*>(SpawnedActor) : nullptr;
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Components;
|
||||||
|
SpawnedActor->GetComponents(Components);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == CompName)
|
||||||
|
{
|
||||||
|
Target = Comp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Target)
|
||||||
|
{
|
||||||
|
FProperty* Prop = FindFProperty<FProperty>(Target->GetClass(), *PropName);
|
||||||
|
if (Prop)
|
||||||
|
{
|
||||||
|
Prop->ImportText_InContainer(**ValueStr, Target, Target, PPF_None);
|
||||||
|
if (UActorComponent* AsComp = Cast<UActorComponent>(Target))
|
||||||
|
{
|
||||||
|
AsComp->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SpawnedCount++;
|
SpawnedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
|
||||||
|
UPS_Editor_EditableComponent::UPS_Editor_EditableComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Internal helpers ----
|
||||||
|
|
||||||
|
static UClass* FindOwnerClass(const UActorComponent* Comp)
|
||||||
|
{
|
||||||
|
if (AActor* Owner = Comp->GetOwner())
|
||||||
|
{
|
||||||
|
return Owner->GetClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UObject* Outer = Comp->GetOuter(); Outer; Outer = Outer->GetOuter())
|
||||||
|
{
|
||||||
|
if (AActor* AsActor = Cast<AActor>(Outer))
|
||||||
|
{
|
||||||
|
return AsActor->GetClass();
|
||||||
|
}
|
||||||
|
if (UClass* AsClass = Cast<UClass>(Outer))
|
||||||
|
{
|
||||||
|
if (AsClass->IsChildOf(AActor::StaticClass()))
|
||||||
|
{
|
||||||
|
return AsClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (UBlueprint* AsBP = Cast<UBlueprint>(Outer))
|
||||||
|
{
|
||||||
|
return AsBP->GeneratedClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TArray<UActorComponent*> CollectComponentTemplates(UClass* ActorClass, const UActorComponent* Exclude)
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> Result;
|
||||||
|
if (!ActorClass) return Result;
|
||||||
|
|
||||||
|
if (AActor* CDO = Cast<AActor>(ActorClass->GetDefaultObject()))
|
||||||
|
{
|
||||||
|
TArray<UActorComponent*> CDOComps;
|
||||||
|
CDO->GetComponents(CDOComps);
|
||||||
|
for (UActorComponent* Comp : CDOComps)
|
||||||
|
{
|
||||||
|
if (Comp && Comp != Exclude)
|
||||||
|
{
|
||||||
|
Result.Add(Comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<UObject*> SubObjects;
|
||||||
|
GetObjectsWithOuter(ActorClass, SubObjects, true);
|
||||||
|
for (UObject* Sub : SubObjects)
|
||||||
|
{
|
||||||
|
UActorComponent* Comp = Cast<UActorComponent>(Sub);
|
||||||
|
if (Comp && Comp != Exclude && !Result.Contains(Comp))
|
||||||
|
{
|
||||||
|
Result.Add(Comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Strip Blueprint template suffixes (e.g. "_GEN_VARIABLE") so stored names match runtime names. */
|
||||||
|
static FString CleanComponentName(const FString& Name)
|
||||||
|
{
|
||||||
|
FString Clean = Name;
|
||||||
|
Clean.RemoveFromEnd(TEXT("_GEN_VARIABLE"));
|
||||||
|
return Clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Dropdown functions ----
|
||||||
|
|
||||||
|
TArray<FString> UPS_Editor_EditableComponent::GetAvailableComponentNames() const
|
||||||
|
{
|
||||||
|
TArray<FString> Names;
|
||||||
|
Names.Add(TEXT("(Actor)")); // Actor-level properties
|
||||||
|
|
||||||
|
UClass* OwnerClass = FindOwnerClass(this);
|
||||||
|
if (!OwnerClass) return Names;
|
||||||
|
|
||||||
|
TArray<UActorComponent*> Components = CollectComponentTemplates(OwnerClass, this);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
Names.AddUnique(CleanComponentName(Comp->GetName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Names.Sort();
|
||||||
|
return Names;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FString> UPS_Editor_EditableComponent::GetFilteredPropertyNames() const
|
||||||
|
{
|
||||||
|
TArray<FString> Names;
|
||||||
|
Names.Add(TEXT("")); // Allow clearing
|
||||||
|
|
||||||
|
UClass* OwnerClass = FindOwnerClass(this);
|
||||||
|
if (!OwnerClass) return Names;
|
||||||
|
|
||||||
|
UClass* TargetClass = nullptr;
|
||||||
|
const FString SelComp = SelectedComponent.ToString();
|
||||||
|
|
||||||
|
if (SelectedComponent.IsNone() || SelComp.IsEmpty() || SelComp == TEXT("(Actor)"))
|
||||||
|
{
|
||||||
|
// Actor-level properties
|
||||||
|
TargetClass = OwnerClass;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Find the component's class (match cleaned names)
|
||||||
|
TArray<UActorComponent*> Components = CollectComponentTemplates(OwnerClass, this);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
if (CleanComponentName(Comp->GetName()) == SelComp)
|
||||||
|
{
|
||||||
|
TargetClass = Comp->GetClass();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetClass) return Names;
|
||||||
|
|
||||||
|
for (TFieldIterator<FProperty> PropIt(TargetClass, EFieldIterationFlags::IncludeSuper); PropIt; ++PropIt)
|
||||||
|
{
|
||||||
|
FProperty* Prop = *PropIt;
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_Edit))
|
||||||
|
{
|
||||||
|
Names.AddUnique(Prop->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Names.Sort();
|
||||||
|
return Names;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
void UPS_Editor_EditableComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||||
|
{
|
||||||
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||||
|
|
||||||
|
if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UPS_Editor_EditableComponent, AddProperty))
|
||||||
|
{
|
||||||
|
if (!AddProperty.IsNone())
|
||||||
|
{
|
||||||
|
const FString SelComp = SelectedComponent.ToString();
|
||||||
|
const bool bIsActor = SelectedComponent.IsNone() || SelComp.IsEmpty() || SelComp == TEXT("(Actor)");
|
||||||
|
|
||||||
|
// Build full path: "ComponentName.PropertyName" or just "PropertyName"
|
||||||
|
FName FullPath;
|
||||||
|
if (bIsActor)
|
||||||
|
{
|
||||||
|
FullPath = AddProperty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FullPath = FName(SelComp + TEXT(".") + AddProperty.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EditableProperties.Contains(FullPath))
|
||||||
|
{
|
||||||
|
EditableProperties.Add(FullPath);
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Added editable property: %s"), *FullPath.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
AddProperty = NAME_None; // Reset for next pick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ---- Helpers for consumers ----
|
||||||
|
|
||||||
|
void UPS_Editor_EditableComponent::ParsePropertyPath(const FName& Path, FString& OutComponentName, FString& OutPropertyName)
|
||||||
|
{
|
||||||
|
const FString PathStr = Path.ToString();
|
||||||
|
if (!PathStr.Split(TEXT("."), &OutComponentName, &OutPropertyName))
|
||||||
|
{
|
||||||
|
OutComponentName = TEXT("");
|
||||||
|
OutPropertyName = PathStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_EditableComponent::GetDisplayName(const FName& Path) const
|
||||||
|
{
|
||||||
|
if (const FString* Override = DisplayNames.Find(Path))
|
||||||
|
{
|
||||||
|
if (!Override->IsEmpty())
|
||||||
|
{
|
||||||
|
return *Override;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString CompName, PropName;
|
||||||
|
ParsePropertyPath(Path, CompName, PropName);
|
||||||
|
return PropName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "PS_Editor_EditableComponent.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker component that declares which properties are editable in the PS_Editor runtime editor.
|
||||||
|
* Separate from SpawnableComponent (which handles catalogue metadata).
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Pick a component from "Select Component"
|
||||||
|
* 2. Pick a property from "Add Property" (filtered to that component)
|
||||||
|
* 3. It auto-adds to "Editable Properties" as "Component.Property"
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (PSEditor), meta = (BlueprintSpawnableComponent, DisplayName = "PS Editor Editable"))
|
||||||
|
class PS_EDITOR_API UPS_Editor_EditableComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPS_Editor_EditableComponent();
|
||||||
|
|
||||||
|
/** Step 1: Pick a component (leave empty for actor-level properties). */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Add Property", meta = (GetOptions = "GetAvailableComponentNames"))
|
||||||
|
FName SelectedComponent;
|
||||||
|
|
||||||
|
/** Step 2: Pick a property from the selected component. Auto-adds to list below. */
|
||||||
|
UPROPERTY(EditAnywhere, Category = "Add Property", meta = (GetOptions = "GetFilteredPropertyNames"))
|
||||||
|
FName AddProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties exposed in the runtime editor.
|
||||||
|
* Format: "ComponentName.PropertyName" or just "PropertyName" for actor-level.
|
||||||
|
* Populated via the dropdowns above, or editable manually.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties")
|
||||||
|
TArray<FName> EditableProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional display name overrides for the runtime editor UI.
|
||||||
|
* Key = property path (same as in EditableProperties), Value = label shown in UI.
|
||||||
|
* If not set, the property name is used as label.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Editable Properties")
|
||||||
|
TMap<FName, FString> DisplayNames;
|
||||||
|
|
||||||
|
/** Lists available component names on the owning actor. */
|
||||||
|
UFUNCTION()
|
||||||
|
TArray<FString> GetAvailableComponentNames() const;
|
||||||
|
|
||||||
|
/** Lists editable properties for the currently SelectedComponent. */
|
||||||
|
UFUNCTION()
|
||||||
|
TArray<FString> GetFilteredPropertyNames() const;
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ---- Helpers for consumers ----
|
||||||
|
|
||||||
|
/** Parse a property path into component name and property name. */
|
||||||
|
static void ParsePropertyPath(const FName& Path, FString& OutComponentName, FString& OutPropertyName);
|
||||||
|
|
||||||
|
/** Get the display name for a property path. */
|
||||||
|
FString GetDisplayName(const FName& Path) const;
|
||||||
|
};
|
||||||
@@ -211,6 +211,20 @@ void UPS_Editor_SpawnManager::TrackSpawnedActor(AActor* Actor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_SpawnManager::IsActorSpawned(AActor* Actor) const
|
||||||
|
{
|
||||||
|
if (!Actor) return false;
|
||||||
|
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||||
|
{
|
||||||
|
if (Weak.Get() == Actor)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
TArray<FString> UPS_Editor_SpawnManager::GetCategories() const
|
||||||
{
|
{
|
||||||
TArray<FString> Categories;
|
TArray<FString> Categories;
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ public:
|
|||||||
/** Register an externally-spawned actor for tracking (used by duplicate/paste). */
|
/** Register an externally-spawned actor for tracking (used by duplicate/paste). */
|
||||||
void TrackSpawnedActor(AActor* Actor);
|
void TrackSpawnedActor(AActor* Actor);
|
||||||
|
|
||||||
|
/** Returns true if this actor was spawned/tracked by the editor (catalogue, duplicate, paste, load). */
|
||||||
|
bool IsActorSpawned(AActor* Actor) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||||
|
|||||||
@@ -4,15 +4,19 @@
|
|||||||
#include "PS_Editor_SceneSerializer.h"
|
#include "PS_Editor_SceneSerializer.h"
|
||||||
#include "PS_Editor_UndoManager.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_EditableComponent.h"
|
||||||
|
#include "PS_Editor_SpawnableComponent.h"
|
||||||
#include "Widgets/SBoxPanel.h"
|
#include "Widgets/SBoxPanel.h"
|
||||||
#include "Widgets/Text/STextBlock.h"
|
#include "Widgets/Text/STextBlock.h"
|
||||||
#include "Widgets/Layout/SBorder.h"
|
#include "Widgets/Layout/SBorder.h"
|
||||||
#include "Widgets/Layout/SScrollBox.h"
|
#include "Widgets/Layout/SScrollBox.h"
|
||||||
#include "Widgets/Input/SButton.h"
|
#include "Widgets/Input/SButton.h"
|
||||||
#include "Widgets/Images/SImage.h"
|
#include "Widgets/Images/SImage.h"
|
||||||
|
#include "Widgets/Input/SCheckBox.h"
|
||||||
#include "Engine/Texture2D.h"
|
#include "Engine/Texture2D.h"
|
||||||
#include "Widgets/Input/SEditableTextBox.h"
|
#include "Widgets/Input/SEditableTextBox.h"
|
||||||
#include "Widgets/SOverlay.h"
|
#include "Widgets/SOverlay.h"
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
||||||
{
|
{
|
||||||
@@ -92,13 +96,24 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
[
|
[
|
||||||
BuildSpawnCatalog()
|
BuildSpawnCatalog()
|
||||||
]
|
]
|
||||||
// Right panel: Properties
|
// Right panel: Properties + Outliner
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
.HAlign(HAlign_Right)
|
.HAlign(HAlign_Right)
|
||||||
.VAlign(VAlign_Top)
|
.VAlign(VAlign_Top)
|
||||||
.Padding(16.0f)
|
.Padding(16.0f, 16.0f, 16.0f, 60.0f)
|
||||||
[
|
[
|
||||||
BuildPropertiesPanel()
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot()
|
||||||
|
.AutoHeight()
|
||||||
|
[
|
||||||
|
BuildPropertiesPanel()
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot()
|
||||||
|
.FillHeight(1.0f)
|
||||||
|
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
BuildOutliner()
|
||||||
|
]
|
||||||
]
|
]
|
||||||
// Bottom help bar
|
// Bottom help bar
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
@@ -328,6 +343,10 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildPropertiesPanel()
|
|||||||
]
|
]
|
||||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||||
[ MakeRow(FText::GetEmpty(), ScaX, ScaY, ScaZ, RedX, GreenY, BlueZ) ]
|
[ MakeRow(FText::GetEmpty(), ScaX, ScaY, ScaZ, RedX, GreenY, BlueZ) ]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SAssignNew(DynamicPropertiesContainer, SVerticalBox)
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,6 +668,7 @@ void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDelt
|
|||||||
{
|
{
|
||||||
Super::NativeTick(MyGeometry, InDeltaTime);
|
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||||
RefreshPropertiesFromSelection();
|
RefreshPropertiesFromSelection();
|
||||||
|
RefreshOutliner();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||||
@@ -702,6 +722,13 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
ClearField(LocX); ClearField(LocY); ClearField(LocZ);
|
ClearField(LocX); ClearField(LocY); ClearField(LocZ);
|
||||||
ClearField(RotX); ClearField(RotY); ClearField(RotZ);
|
ClearField(RotX); ClearField(RotY); ClearField(RotZ);
|
||||||
ClearField(ScaX); ClearField(ScaY); ClearField(ScaZ);
|
ClearField(ScaX); ClearField(ScaY); ClearField(ScaZ);
|
||||||
|
|
||||||
|
if (LastSelectedActor.IsValid())
|
||||||
|
{
|
||||||
|
LastSelectedActor = nullptr;
|
||||||
|
RebuildDynamicProperties();
|
||||||
|
LastOutlinerActorCount = -1; // Force outliner refresh
|
||||||
|
}
|
||||||
bIsRefreshing = false;
|
bIsRefreshing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -737,6 +764,16 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
SetField(RotX, Rot.Roll); SetField(RotY, Rot.Pitch); SetField(RotZ, Rot.Yaw);
|
SetField(RotX, Rot.Roll); SetField(RotY, Rot.Pitch); SetField(RotZ, Rot.Yaw);
|
||||||
SetField(ScaX, Sca.X); SetField(ScaY, Sca.Y); SetField(ScaZ, Sca.Z);
|
SetField(ScaX, Sca.X); SetField(ScaY, Sca.Y); SetField(ScaZ, Sca.Z);
|
||||||
|
|
||||||
|
// Detect selection change → rebuild dynamic properties + refresh outliner highlight
|
||||||
|
if (Actor != LastSelectedActor.Get())
|
||||||
|
{
|
||||||
|
LastSelectedActor = Actor;
|
||||||
|
RebuildDynamicProperties();
|
||||||
|
LastOutlinerActorCount = -1; // Force outliner refresh for highlight update
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshDynamicPropertyValues();
|
||||||
|
|
||||||
bIsRefreshing = false;
|
bIsRefreshing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,6 +822,496 @@ void UPS_Editor_MainWidget::ApplyTransformFromUI()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildOutliner()
|
||||||
|
{
|
||||||
|
return SNew(SBorder)
|
||||||
|
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||||
|
.Padding(FMargin(10.0f, 8.0f))
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight()
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Scene Objects")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SScrollBox)
|
||||||
|
+ SScrollBox::Slot()
|
||||||
|
[
|
||||||
|
SAssignNew(OutlinerListContainer, SVerticalBox)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::RefreshOutliner()
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = SpawnManager.Get();
|
||||||
|
if (!SM || !OutlinerListContainer.IsValid()) return;
|
||||||
|
|
||||||
|
// Count visible (non-hidden) spawned actors
|
||||||
|
const TArray<TWeakObjectPtr<AActor>>& SpawnedActors = SM->GetSpawnedActors();
|
||||||
|
int32 VisibleCount = 0;
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SpawnedActors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
if (!Actor->IsHidden()) VisibleCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only rebuild if count changed (avoid per-tick rebuild)
|
||||||
|
if (VisibleCount == LastOutlinerActorCount) return;
|
||||||
|
LastOutlinerActorCount = VisibleCount;
|
||||||
|
|
||||||
|
OutlinerListContainer->ClearChildren();
|
||||||
|
|
||||||
|
UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get();
|
||||||
|
const FSlateFontInfo ItemFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
|
|
||||||
|
for (int32 i = 0; i < SpawnedActors.Num(); i++)
|
||||||
|
{
|
||||||
|
AActor* Actor = SpawnedActors[i].Get();
|
||||||
|
if (!Actor || Actor->IsHidden()) continue;
|
||||||
|
|
||||||
|
// Build display name: SpawnableComponent DisplayName > Class name (cleaned)
|
||||||
|
FString DisplayName;
|
||||||
|
if (UActorComponent* SpawnComp = Actor->FindComponentByClass(UPS_Editor_SpawnableComponent::StaticClass()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnableComponent* SC = Cast<UPS_Editor_SpawnableComponent>(SpawnComp))
|
||||||
|
{
|
||||||
|
if (!SC->DisplayName.IsEmpty())
|
||||||
|
{
|
||||||
|
DisplayName = SC->DisplayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (DisplayName.IsEmpty())
|
||||||
|
{
|
||||||
|
DisplayName = Actor->GetClass()->GetName();
|
||||||
|
DisplayName.RemoveFromEnd(TEXT("_C"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append index for disambiguation
|
||||||
|
DisplayName = FString::Printf(TEXT("%s #%d"), *DisplayName, i + 1);
|
||||||
|
|
||||||
|
const bool bIsSelected = SelMgr && SelMgr->IsActorSelected(Actor);
|
||||||
|
const FLinearColor TextColor = bIsSelected ? FLinearColor(0.2f, 0.7f, 1.0f) : FLinearColor(0.7f, 0.7f, 0.7f);
|
||||||
|
const FLinearColor BgColor = bIsSelected ? FLinearColor(0.15f, 0.25f, 0.35f, 0.8f) : FLinearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
|
TWeakObjectPtr<AActor> WeakActor = Actor;
|
||||||
|
|
||||||
|
OutlinerListContainer->AddSlot().AutoHeight().Padding(0.0f, 1.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.ButtonStyle(FCoreStyle::Get(), "NoBorder")
|
||||||
|
.ContentPadding(FMargin(6.0f, 3.0f))
|
||||||
|
.OnClicked_Lambda([this, WeakActor]() -> FReply
|
||||||
|
{
|
||||||
|
AActor* Act = WeakActor.Get();
|
||||||
|
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||||
|
if (Act && SM)
|
||||||
|
{
|
||||||
|
SM->ClearSelection();
|
||||||
|
SM->SelectActor(Act);
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(SBorder)
|
||||||
|
.BorderBackgroundColor(BgColor)
|
||||||
|
.Padding(FMargin(4.0f, 2.0f))
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(DisplayName))
|
||||||
|
.Font(ItemFont)
|
||||||
|
.ColorAndOpacity(TextColor)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* UPS_Editor_MainWidget::FindPropertyTarget(AActor* Actor, const FString& ComponentName)
|
||||||
|
{
|
||||||
|
if (!Actor) return nullptr;
|
||||||
|
if (ComponentName.IsEmpty()) return Actor;
|
||||||
|
|
||||||
|
TArray<UActorComponent*> Components;
|
||||||
|
Actor->GetComponents(Components);
|
||||||
|
for (UActorComponent* Comp : Components)
|
||||||
|
{
|
||||||
|
if (Comp && Comp->GetName() == ComponentName)
|
||||||
|
{
|
||||||
|
return Comp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::RebuildDynamicProperties()
|
||||||
|
{
|
||||||
|
DynamicPropertyRows.Empty();
|
||||||
|
if (!DynamicPropertiesContainer.IsValid()) return;
|
||||||
|
DynamicPropertiesContainer->ClearChildren();
|
||||||
|
|
||||||
|
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||||
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
AActor* Actor = SM->GetSelectedActors()[0].Get();
|
||||||
|
if (!Actor) return;
|
||||||
|
|
||||||
|
UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass<UPS_Editor_EditableComponent>();
|
||||||
|
if (!EditComp)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No EditableComponent on %s"), *Actor->GetName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (EditComp->EditableProperties.Num() == 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName());
|
||||||
|
|
||||||
|
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
|
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
|
|
||||||
|
// Separator line
|
||||||
|
DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 6.0f, 0.0f, 2.0f)
|
||||||
|
[
|
||||||
|
SNew(SBorder)
|
||||||
|
.BorderBackgroundColor(FLinearColor(0.3f, 0.3f, 0.3f, 0.5f))
|
||||||
|
.Padding(FMargin(0.0f, 1.0f))
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Properties")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f))
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
for (int32 i = 0; i < EditComp->EditableProperties.Num(); i++)
|
||||||
|
{
|
||||||
|
const FName& Path = EditComp->EditableProperties[i];
|
||||||
|
|
||||||
|
FPS_Editor_PropertyRowWidgets Row;
|
||||||
|
UPS_Editor_EditableComponent::ParsePropertyPath(Path, Row.ComponentName, Row.PropertyName);
|
||||||
|
Row.DisplayName = EditComp->GetDisplayName(Path);
|
||||||
|
Row.Key = Path.ToString();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName);
|
||||||
|
|
||||||
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
// Log available components for debugging
|
||||||
|
TArray<UActorComponent*> AllComps;
|
||||||
|
Actor->GetComponents(AllComps);
|
||||||
|
for (UActorComponent* C : AllComps)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Available component: '%s' (%s)"), *C->GetName(), *C->GetClass()->GetName());
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Component '%s' not found on %s"), *Row.ComponentName, *Actor->GetName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Row.CachedProperty = FindFProperty<FProperty>(Target->GetClass(), *Row.PropertyName);
|
||||||
|
if (!Row.CachedProperty)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved OK → %s on %s"), *Row.CachedProperty->GetName(), *Target->GetClass()->GetName());
|
||||||
|
|
||||||
|
const int32 RowIndex = DynamicPropertyRows.Num();
|
||||||
|
|
||||||
|
// Label
|
||||||
|
DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 3.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(Row.DisplayName))
|
||||||
|
.Font(LabelFont)
|
||||||
|
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||||
|
];
|
||||||
|
|
||||||
|
// Value widget depends on property type
|
||||||
|
TSharedRef<SWidget> ValueWidget = SNullWidget::NullWidget;
|
||||||
|
|
||||||
|
if (CastField<FBoolProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
ValueWidget = SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth()
|
||||||
|
[
|
||||||
|
SAssignNew(Row.BoolField, SCheckBox)
|
||||||
|
.OnCheckStateChanged_Lambda([this, RowIndex](ECheckBoxState)
|
||||||
|
{
|
||||||
|
ApplyPropertyFromUI(RowIndex);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
else if (FStructProperty* StructProp = CastField<FStructProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
auto MakeTextField = [&](TSharedPtr<SEditableTextBox>& Field) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
return SAssignNew(Field, SEditableTextBox)
|
||||||
|
.Font(ValueFont)
|
||||||
|
.MinDesiredWidth(55.0f)
|
||||||
|
.OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type)
|
||||||
|
{
|
||||||
|
ApplyPropertyFromUI(RowIndex);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (StructProp->Struct == TBaseStructure<FVector>::Get()
|
||||||
|
|| StructProp->Struct == TBaseStructure<FRotator>::Get())
|
||||||
|
{
|
||||||
|
ValueWidget = SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
|
[ MakeTextField(Row.VecX) ]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
|
[ MakeTextField(Row.VecY) ]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
|
[ MakeTextField(Row.VecZ) ];
|
||||||
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FLinearColor>::Get())
|
||||||
|
{
|
||||||
|
ValueWidget = SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
|
[ MakeTextField(Row.ColR) ]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
|
[ MakeTextField(Row.ColG) ]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
|
[ MakeTextField(Row.ColB) ]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f)
|
||||||
|
[ MakeTextField(Row.ColA) ];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unsupported struct: single text field with ExportText format
|
||||||
|
ValueWidget = SAssignNew(Row.SingleField, SEditableTextBox)
|
||||||
|
.Font(ValueFont)
|
||||||
|
.MinDesiredWidth(70.0f)
|
||||||
|
.OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type)
|
||||||
|
{
|
||||||
|
ApplyPropertyFromUI(RowIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default: single text field (float, int, string, etc.)
|
||||||
|
ValueWidget = SAssignNew(Row.SingleField, SEditableTextBox)
|
||||||
|
.Font(ValueFont)
|
||||||
|
.MinDesiredWidth(70.0f)
|
||||||
|
.OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type)
|
||||||
|
{
|
||||||
|
ApplyPropertyFromUI(RowIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 2.0f)
|
||||||
|
[ ValueWidget ];
|
||||||
|
|
||||||
|
DynamicPropertyRows.Add(Row);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
||||||
|
{
|
||||||
|
if (DynamicPropertyRows.Num() == 0) return;
|
||||||
|
|
||||||
|
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||||
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
AActor* Actor = SM->GetSelectedActors()[0].Get();
|
||||||
|
if (!Actor) return;
|
||||||
|
|
||||||
|
for (FPS_Editor_PropertyRowWidgets& Row : DynamicPropertyRows)
|
||||||
|
{
|
||||||
|
if (!Row.CachedProperty) continue;
|
||||||
|
|
||||||
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
|
if (!Target) continue;
|
||||||
|
|
||||||
|
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
if (Row.BoolField.IsValid())
|
||||||
|
{
|
||||||
|
const bool Val = BoolProp->GetPropertyValue_InContainer(Target);
|
||||||
|
Row.BoolField->SetIsChecked(Val ? ECheckBoxState::Checked : ECheckBoxState::Unchecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (FStructProperty* StructProp = CastField<FStructProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
void* ValuePtr = Row.CachedProperty->ContainerPtrToValuePtr<void>(Target);
|
||||||
|
if (!ValuePtr) continue;
|
||||||
|
|
||||||
|
auto SetIfNoFocus = [this](TSharedPtr<SEditableTextBox>& Field, float Val)
|
||||||
|
{
|
||||||
|
if (Field.IsValid() && !Field->HasKeyboardFocus())
|
||||||
|
{
|
||||||
|
Field->SetText(FText::FromString(FloatToString(Val)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (StructProp->Struct == TBaseStructure<FVector>::Get())
|
||||||
|
{
|
||||||
|
const FVector& Vec = *static_cast<const FVector*>(ValuePtr);
|
||||||
|
SetIfNoFocus(Row.VecX, Vec.X);
|
||||||
|
SetIfNoFocus(Row.VecY, Vec.Y);
|
||||||
|
SetIfNoFocus(Row.VecZ, Vec.Z);
|
||||||
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FRotator>::Get())
|
||||||
|
{
|
||||||
|
const FRotator& Rot = *static_cast<const FRotator*>(ValuePtr);
|
||||||
|
SetIfNoFocus(Row.VecX, Rot.Pitch);
|
||||||
|
SetIfNoFocus(Row.VecY, Rot.Yaw);
|
||||||
|
SetIfNoFocus(Row.VecZ, Rot.Roll);
|
||||||
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FLinearColor>::Get())
|
||||||
|
{
|
||||||
|
const FLinearColor& Col = *static_cast<const FLinearColor*>(ValuePtr);
|
||||||
|
SetIfNoFocus(Row.ColR, Col.R);
|
||||||
|
SetIfNoFocus(Row.ColG, Col.G);
|
||||||
|
SetIfNoFocus(Row.ColB, Col.B);
|
||||||
|
SetIfNoFocus(Row.ColA, Col.A);
|
||||||
|
}
|
||||||
|
else if (Row.SingleField.IsValid() && !Row.SingleField->HasKeyboardFocus())
|
||||||
|
{
|
||||||
|
FString ExportedVal;
|
||||||
|
Row.CachedProperty->ExportText_InContainer(0, ExportedVal, Target, Target, nullptr, PPF_None);
|
||||||
|
Row.SingleField->SetText(FText::FromString(ExportedVal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Row.SingleField.IsValid() && !Row.SingleField->HasKeyboardFocus())
|
||||||
|
{
|
||||||
|
// float, double, int, string, etc.
|
||||||
|
if (CastField<FFloatProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
const float Val = *Row.CachedProperty->ContainerPtrToValuePtr<float>(Target);
|
||||||
|
Row.SingleField->SetText(FText::FromString(FloatToString(Val)));
|
||||||
|
}
|
||||||
|
else if (CastField<FDoubleProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
const double Val = *Row.CachedProperty->ContainerPtrToValuePtr<double>(Target);
|
||||||
|
Row.SingleField->SetText(FText::FromString(FloatToString(static_cast<float>(Val))));
|
||||||
|
}
|
||||||
|
else if (CastField<FIntProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
const int32 Val = *Row.CachedProperty->ContainerPtrToValuePtr<int32>(Target);
|
||||||
|
Row.SingleField->SetText(FText::FromString(FString::FromInt(Val)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Generic: ExportText
|
||||||
|
FString ExportedVal;
|
||||||
|
Row.CachedProperty->ExportText_InContainer(0, ExportedVal, Target, Target, nullptr, PPF_None);
|
||||||
|
Row.SingleField->SetText(FText::FromString(ExportedVal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
||||||
|
{
|
||||||
|
if (bIsRefreshing) return;
|
||||||
|
if (!DynamicPropertyRows.IsValidIndex(RowIndex)) return;
|
||||||
|
|
||||||
|
FPS_Editor_PropertyRowWidgets& Row = DynamicPropertyRows[RowIndex];
|
||||||
|
if (!Row.CachedProperty) return;
|
||||||
|
|
||||||
|
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||||
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
AActor* Actor = SM->GetSelectedActors()[0].Get();
|
||||||
|
if (!Actor) return;
|
||||||
|
|
||||||
|
UObject* Target = FindPropertyTarget(Actor, Row.ComponentName);
|
||||||
|
if (!Target) return;
|
||||||
|
|
||||||
|
// Export old value for undo
|
||||||
|
FString OldValue;
|
||||||
|
Row.CachedProperty->ExportText_InContainer(0, OldValue, Target, Target, nullptr, PPF_None);
|
||||||
|
|
||||||
|
// Build new value text from UI and import
|
||||||
|
FString NewValueText;
|
||||||
|
|
||||||
|
if (CastField<FBoolProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
NewValueText = Row.BoolField.IsValid() && Row.BoolField->IsChecked() ? TEXT("True") : TEXT("False");
|
||||||
|
}
|
||||||
|
else if (FStructProperty* StructProp = CastField<FStructProperty>(Row.CachedProperty))
|
||||||
|
{
|
||||||
|
auto GetText = [](TSharedPtr<SEditableTextBox>& F) -> FString
|
||||||
|
{
|
||||||
|
return F.IsValid() ? F->GetText().ToString() : TEXT("0");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (StructProp->Struct == TBaseStructure<FVector>::Get())
|
||||||
|
{
|
||||||
|
NewValueText = FString::Printf(TEXT("(X=%s,Y=%s,Z=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ));
|
||||||
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FRotator>::Get())
|
||||||
|
{
|
||||||
|
NewValueText = FString::Printf(TEXT("(Pitch=%s,Yaw=%s,Roll=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ));
|
||||||
|
}
|
||||||
|
else if (StructProp->Struct == TBaseStructure<FLinearColor>::Get())
|
||||||
|
{
|
||||||
|
NewValueText = FString::Printf(TEXT("(R=%s,G=%s,B=%s,A=%s)"), *GetText(Row.ColR), *GetText(Row.ColG), *GetText(Row.ColB), *GetText(Row.ColA));
|
||||||
|
}
|
||||||
|
else if (Row.SingleField.IsValid())
|
||||||
|
{
|
||||||
|
NewValueText = Row.SingleField->GetText().ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Row.SingleField.IsValid())
|
||||||
|
{
|
||||||
|
NewValueText = Row.SingleField->GetText().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NewValueText.IsEmpty()) return;
|
||||||
|
|
||||||
|
// Apply the new value
|
||||||
|
Row.CachedProperty->ImportText_InContainer(*NewValueText, Target, Target, PPF_None);
|
||||||
|
|
||||||
|
// Refresh rendering
|
||||||
|
if (UActorComponent* Comp = Cast<UActorComponent>(Target))
|
||||||
|
{
|
||||||
|
Comp->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export new value after import (normalized form for undo)
|
||||||
|
FString NewValue;
|
||||||
|
Row.CachedProperty->ExportText_InContainer(0, NewValue, Target, Target, nullptr, PPF_None);
|
||||||
|
|
||||||
|
// Record undo
|
||||||
|
if (APlayerController* PC = GetOwningPlayer())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_PropertyAction> Action = MakeShared<FPS_Editor_PropertyAction>();
|
||||||
|
Action->Actor = Actor;
|
||||||
|
Action->ComponentName = Row.ComponentName;
|
||||||
|
Action->PropertyName = Row.PropertyName;
|
||||||
|
Action->OldValue = OldValue;
|
||||||
|
Action->NewValue = NewValue;
|
||||||
|
Action->Description = FString::Printf(TEXT("Edit %s"), *Row.DisplayName);
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue);
|
||||||
|
}
|
||||||
|
|
||||||
FString UPS_Editor_MainWidget::FloatToString(float Value) const
|
FString UPS_Editor_MainWidget::FloatToString(float Value) const
|
||||||
{
|
{
|
||||||
return FString::Printf(TEXT("%.2f"), Value);
|
return FString::Printf(TEXT("%.2f"), Value);
|
||||||
|
|||||||
@@ -9,6 +9,22 @@ class UPS_Editor_SelectionManager;
|
|||||||
class UPS_Editor_SpawnManager;
|
class UPS_Editor_SpawnManager;
|
||||||
class UPS_Editor_SceneSerializer;
|
class UPS_Editor_SceneSerializer;
|
||||||
|
|
||||||
|
/** Widget state for a single dynamic property row in the property panel. */
|
||||||
|
struct FPS_Editor_PropertyRowWidgets
|
||||||
|
{
|
||||||
|
FString ComponentName;
|
||||||
|
FString PropertyName;
|
||||||
|
FString DisplayName;
|
||||||
|
FString Key; // "ComponentName.PropertyName"
|
||||||
|
FProperty* CachedProperty = nullptr;
|
||||||
|
|
||||||
|
// Widget refs (only relevant ones are used based on property type)
|
||||||
|
TSharedPtr<SEditableTextBox> SingleField; // float, double, int32, FString
|
||||||
|
TSharedPtr<SCheckBox> BoolField; // bool
|
||||||
|
TSharedPtr<SEditableTextBox> VecX, VecY, VecZ; // FVector, FRotator
|
||||||
|
TSharedPtr<SEditableTextBox> ColR, ColG, ColB, ColA; // FLinearColor
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main editor widget for PS_Editor.
|
* Main editor widget for PS_Editor.
|
||||||
* Contains: title bar, transform mode toolbar, properties panel, help bar.
|
* Contains: title bar, transform mode toolbar, properties panel, help bar.
|
||||||
@@ -62,15 +78,30 @@ private:
|
|||||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
TSharedPtr<SEditableTextBox> ScaX, ScaY, ScaZ;
|
TSharedPtr<SEditableTextBox> ScaX, ScaY, ScaZ;
|
||||||
|
|
||||||
|
// Dynamic property editing
|
||||||
|
TSharedPtr<SVerticalBox> DynamicPropertiesContainer;
|
||||||
|
TArray<FPS_Editor_PropertyRowWidgets> DynamicPropertyRows;
|
||||||
|
TWeakObjectPtr<AActor> LastSelectedActor;
|
||||||
|
|
||||||
|
// Outliner
|
||||||
|
TSharedPtr<SVerticalBox> OutlinerListContainer;
|
||||||
|
int32 LastOutlinerActorCount = -1;
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
TSharedRef<SWidget> BuildToolbar();
|
TSharedRef<SWidget> BuildToolbar();
|
||||||
TSharedRef<SWidget> BuildPropertiesPanel();
|
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||||
TSharedRef<SWidget> BuildSpawnCatalog();
|
TSharedRef<SWidget> BuildSpawnCatalog();
|
||||||
TSharedRef<SWidget> BuildSceneButtons();
|
TSharedRef<SWidget> BuildSceneButtons();
|
||||||
|
TSharedRef<SWidget> BuildOutliner();
|
||||||
|
void RefreshOutliner();
|
||||||
void PopulateSceneList();
|
void PopulateSceneList();
|
||||||
void PopulateSpawnCatalog();
|
void PopulateSpawnCatalog();
|
||||||
void RefreshPropertiesFromSelection();
|
void RefreshPropertiesFromSelection();
|
||||||
void ApplyTransformFromUI();
|
void ApplyTransformFromUI();
|
||||||
|
void RebuildDynamicProperties();
|
||||||
|
void RefreshDynamicPropertyValues();
|
||||||
|
void ApplyPropertyFromUI(int32 RowIndex);
|
||||||
|
static UObject* FindPropertyTarget(AActor* Actor, const FString& ComponentName);
|
||||||
FString FloatToString(float Value) const;
|
FString FloatToString(float Value) const;
|
||||||
|
|
||||||
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
|
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
|
||||||
|
|||||||
Reference in New Issue
Block a user