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.
@@ -3,10 +3,89 @@
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.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)
|
||||
{
|
||||
@@ -53,9 +132,15 @@ void UPS_Editor_SelectionManager::HandleClickAtScreenPosition(FVector2D ScreenPo
|
||||
{
|
||||
AActor* HitActor = Hit.GetActor();
|
||||
|
||||
// Only allow selection of Movable actors (skip static scene geometry like floors/walls)
|
||||
USceneComponent* RootComp = HitActor->GetRootComponent();
|
||||
const bool bIsSelectable = RootComp && RootComp->Mobility == EComponentMobility::Movable;
|
||||
// Only allow selection of actors spawned by the editor (catalogue, duplicate, paste, load)
|
||||
bool bIsSelectable = false;
|
||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||
{
|
||||
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||
{
|
||||
bIsSelectable = SpawnMgr->IsActorSpawned(HitActor);
|
||||
}
|
||||
}
|
||||
|
||||
if (bIsSelectable)
|
||||
{
|
||||
@@ -410,6 +495,10 @@ void UPS_Editor_SelectionManager::DuplicateSelection()
|
||||
{
|
||||
Root->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
|
||||
// Copy custom editable properties from original
|
||||
ImportEditableProps(NewActor, ExportEditableProps(Original));
|
||||
|
||||
NewActors.Add(NewActor);
|
||||
|
||||
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||
@@ -462,6 +551,7 @@ void UPS_Editor_SelectionManager::CopySelection()
|
||||
Entry.RelativeLocation = Actor->GetActorLocation() - Pivot;
|
||||
Entry.Rotation = Actor->GetActorRotation();
|
||||
Entry.Scale = Actor->GetActorScale3D();
|
||||
Entry.CustomProperties = ExportEditableProps(Actor);
|
||||
Clipboard.Add(Entry);
|
||||
}
|
||||
|
||||
@@ -506,6 +596,10 @@ void UPS_Editor_SelectionManager::PasteClipboard()
|
||||
{
|
||||
Root->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
|
||||
// Restore custom editable properties
|
||||
ImportEditableProps(NewActor, Entry.CustomProperties);
|
||||
|
||||
NewActors.Add(NewActor);
|
||||
|
||||
if (UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager())
|
||||
|
||||
@@ -17,6 +17,7 @@ struct FPS_Editor_ClipboardEntry
|
||||
FVector RelativeLocation = FVector::ZeroVector; // Relative to clipboard pivot
|
||||
FRotator Rotation = FRotator::ZeroRotator;
|
||||
FVector Scale = FVector::OneVector;
|
||||
TMap<FString, FString> CustomProperties; // Editable property values
|
||||
};
|
||||
|
||||
DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged);
|
||||
|
||||
@@ -1,4 +1,44 @@
|
||||
#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)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,8 @@ enum class EPS_Editor_ActionType : uint8
|
||||
{
|
||||
Transform,
|
||||
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.
|
||||
* Maintains a linear action stack with a cursor.
|
||||
|
||||
@@ -21,6 +21,10 @@ struct FPS_Editor_ActorData
|
||||
|
||||
UPROPERTY()
|
||||
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. */
|
||||
@@ -30,7 +34,7 @@ struct FPS_Editor_SceneData
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
int32 Version = 1;
|
||||
int32 Version = 2;
|
||||
|
||||
UPROPERTY()
|
||||
FString SceneName;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "PS_Editor_SceneSerializer.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "JsonObjectConverter.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.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)
|
||||
{
|
||||
@@ -33,6 +36,43 @@ bool UPS_Editor_SceneSerializer::SaveScene(const FString& SceneName, UPS_Editor_
|
||||
ActorData.Rotation = Actor->GetActorRotation();
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> Categories;
|
||||
|
||||
@@ -63,6 +63,9 @@ public:
|
||||
/** Register an externally-spawned actor for tracking (used by duplicate/paste). */
|
||||
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:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
#include "PS_Editor_SceneSerializer.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Layout/SScrollBox.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Images/SImage.h"
|
||||
#include "Widgets/Input/SCheckBox.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Widgets/Input/SEditableTextBox.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
|
||||
void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
||||
{
|
||||
@@ -92,13 +96,24 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
[
|
||||
BuildSpawnCatalog()
|
||||
]
|
||||
// Right panel: Properties
|
||||
// Right panel: Properties + Outliner
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Right)
|
||||
.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
|
||||
+ SOverlay::Slot()
|
||||
@@ -328,6 +343,10 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildPropertiesPanel()
|
||||
]
|
||||
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||
[ 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);
|
||||
RefreshPropertiesFromSelection();
|
||||
RefreshOutliner();
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||
@@ -702,6 +722,13 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||
ClearField(LocX); ClearField(LocY); ClearField(LocZ);
|
||||
ClearField(RotX); ClearField(RotY); ClearField(RotZ);
|
||||
ClearField(ScaX); ClearField(ScaY); ClearField(ScaZ);
|
||||
|
||||
if (LastSelectedActor.IsValid())
|
||||
{
|
||||
LastSelectedActor = nullptr;
|
||||
RebuildDynamicProperties();
|
||||
LastOutlinerActorCount = -1; // Force outliner refresh
|
||||
}
|
||||
bIsRefreshing = false;
|
||||
return;
|
||||
}
|
||||
@@ -737,6 +764,16 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||
SetField(RotX, Rot.Roll); SetField(RotY, Rot.Pitch); SetField(RotZ, Rot.Yaw);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
return FString::Printf(TEXT("%.2f"), Value);
|
||||
|
||||
@@ -9,6 +9,22 @@ class UPS_Editor_SelectionManager;
|
||||
class UPS_Editor_SpawnManager;
|
||||
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.
|
||||
* Contains: title bar, transform mode toolbar, properties panel, help bar.
|
||||
@@ -62,15 +78,30 @@ private:
|
||||
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||
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 ----
|
||||
TSharedRef<SWidget> BuildToolbar();
|
||||
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||
TSharedRef<SWidget> BuildSpawnCatalog();
|
||||
TSharedRef<SWidget> BuildSceneButtons();
|
||||
TSharedRef<SWidget> BuildOutliner();
|
||||
void RefreshOutliner();
|
||||
void PopulateSceneList();
|
||||
void PopulateSpawnCatalog();
|
||||
void RefreshPropertiesFromSelection();
|
||||
void ApplyTransformFromUI();
|
||||
void RebuildDynamicProperties();
|
||||
void RefreshDynamicPropertyValues();
|
||||
void ApplyPropertyFromUI(int32 RowIndex);
|
||||
static UObject* FindPropertyTarget(AActor* Actor, const FString& ComponentName);
|
||||
FString FloatToString(float Value) const;
|
||||
|
||||
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
|
||||
|
||||
Reference in New Issue
Block a user