From 50087e5dfb89442c2778b7fcc2c9bfa63e21d73e Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sat, 11 Apr 2026 21:15:07 +0200 Subject: [PATCH] Add runtime property editing, scene outliner, and spawned-only selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Unreal/Content/test.umap | Bin 59857 -> 54794 bytes .../PS_Editor/Content/Cubeezrzea.uasset | Bin 30063 -> 32449 bytes .../Selection/PS_Editor_SelectionManager.cpp | 100 +++- .../Selection/PS_Editor_SelectionManager.h | 1 + .../Selection/PS_Editor_UndoManager.cpp | 40 ++ .../Selection/PS_Editor_UndoManager.h | 26 +- .../Serialization/PS_Editor_SceneData.h | 6 +- .../PS_Editor_SceneSerializer.cpp | 85 +++ .../Spawn/PS_Editor_EditableComponent.cpp | 206 +++++++ .../Spawn/PS_Editor_EditableComponent.h | 67 +++ .../Spawn/PS_Editor_SpawnManager.cpp | 14 + .../PS_Editor/Spawn/PS_Editor_SpawnManager.h | 3 + .../UI/Widgets/PS_Editor_MainWidget.cpp | 533 +++++++++++++++++- .../UI/Widgets/PS_Editor_MainWidget.h | 31 + 14 files changed, 1104 insertions(+), 8 deletions(-) create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp create mode 100644 Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h diff --git a/Unreal/Content/test.umap b/Unreal/Content/test.umap index 1e71822e007973c5131c8b32598ac939a2b447a9..78cdd07d7e5818a45278dd1fd96d9bc560f1de63 100644 GIT binary patch literal 54794 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 F{|Bo1Dl-58 diff --git a/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset b/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset index 6a3523d4d7d45096c56ca80b4662355b7eece662..b96e120ac141dd8407b666000b116bedd9f9c8ad 100644 GIT binary patch delta 10242 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 delta 8442 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 W@)1p@U_9zwXj#IPq>3kV&Hf8petr-D diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp index 8136f40..56ac1c9 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -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 ExportEditableProps(AActor* Actor) +{ + TMap Result; + if (!Actor) return Result; + + UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass(); + 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(Actor) : nullptr; + if (!Target) + { + TArray Comps; + Actor->GetComponents(Comps); + for (UActorComponent* Comp : Comps) + { + if (Comp && Comp->GetName() == CompName) { Target = Comp; break; } + } + } + if (!Target) continue; + + FProperty* Prop = FindFProperty(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& Properties) +{ + if (!Actor || Properties.Num() == 0) return; + + UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass(); + 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(Actor) : nullptr; + if (!Target) + { + TArray Comps; + Actor->GetComponents(Comps); + for (UActorComponent* Comp : Comps) + { + if (Comp && Comp->GetName() == CompName) { Target = Comp; break; } + } + } + if (!Target) continue; + + FProperty* Prop = FindFProperty(Target->GetClass(), *PropName); + if (!Prop) continue; + + Prop->ImportText_InContainer(**ValStr, Target, Target, PPF_None); + if (UActorComponent* AsComp = Cast(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(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()) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h index e2ece7c..0163bf2 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h @@ -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 CustomProperties; // Editable property values }; DECLARE_MULTICAST_DELEGATE(FPS_Editor_OnSelectionChanged); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp index 6135a02..a48a31d 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp @@ -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 Components; + Act->GetComponents(Components); + for (UActorComponent* Comp : Components) + { + if (Comp && Comp->GetName() == ComponentName) + { + TargetObject = Comp; + break; + } + } + } + + if (!TargetObject) return; + + FProperty* Prop = FindFProperty(TargetObject->GetClass(), *PropertyName); + if (!Prop) return; + + Prop->ImportText_InContainer(*ValueStr, TargetObject, TargetObject, PPF_None); + + // Refresh rendering for visual updates + if (UActorComponent* Comp = Cast(TargetObject)) + { + Comp->MarkRenderStateDirty(); + } +} void UPS_Editor_UndoManager::RecordAction(TSharedPtr Action) { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h index c85af53..9362db0 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h @@ -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 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. diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h index 4627fc4..6667745 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneData.h @@ -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 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; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp index 3f9eeaf..7d45e25 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Serialization/PS_Editor_SceneSerializer.cpp @@ -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()) + { + 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(Actor) : nullptr; + if (!Target) + { + TArray Components; + Actor->GetComponents(Components); + for (UActorComponent* Comp : Components) + { + if (Comp && Comp->GetName() == CompName) + { + Target = Comp; + break; + } + } + } + + if (Target) + { + FProperty* Prop = FindFProperty(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()) + { + 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(SpawnedActor) : nullptr; + if (!Target) + { + TArray Components; + SpawnedActor->GetComponents(Components); + for (UActorComponent* Comp : Components) + { + if (Comp && Comp->GetName() == CompName) + { + Target = Comp; + break; + } + } + } + + if (Target) + { + FProperty* Prop = FindFProperty(Target->GetClass(), *PropName); + if (Prop) + { + Prop->ImportText_InContainer(**ValueStr, Target, Target, PPF_None); + if (UActorComponent* AsComp = Cast(Target)) + { + AsComp->MarkRenderStateDirty(); + } + } + } + } + } + } + SpawnedCount++; } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp new file mode 100644 index 0000000..543c260 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.cpp @@ -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(Outer)) + { + return AsActor->GetClass(); + } + if (UClass* AsClass = Cast(Outer)) + { + if (AsClass->IsChildOf(AActor::StaticClass())) + { + return AsClass; + } + } + if (UBlueprint* AsBP = Cast(Outer)) + { + return AsBP->GeneratedClass; + } + } + + return nullptr; +} + +static TArray CollectComponentTemplates(UClass* ActorClass, const UActorComponent* Exclude) +{ + TArray Result; + if (!ActorClass) return Result; + + if (AActor* CDO = Cast(ActorClass->GetDefaultObject())) + { + TArray CDOComps; + CDO->GetComponents(CDOComps); + for (UActorComponent* Comp : CDOComps) + { + if (Comp && Comp != Exclude) + { + Result.Add(Comp); + } + } + } + + TArray SubObjects; + GetObjectsWithOuter(ActorClass, SubObjects, true); + for (UObject* Sub : SubObjects) + { + UActorComponent* Comp = Cast(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 UPS_Editor_EditableComponent::GetAvailableComponentNames() const +{ + TArray Names; + Names.Add(TEXT("(Actor)")); // Actor-level properties + + UClass* OwnerClass = FindOwnerClass(this); + if (!OwnerClass) return Names; + + TArray Components = CollectComponentTemplates(OwnerClass, this); + for (UActorComponent* Comp : Components) + { + Names.AddUnique(CleanComponentName(Comp->GetName())); + } + + Names.Sort(); + return Names; +} + +TArray UPS_Editor_EditableComponent::GetFilteredPropertyNames() const +{ + TArray 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 Components = CollectComponentTemplates(OwnerClass, this); + for (UActorComponent* Comp : Components) + { + if (CleanComponentName(Comp->GetName()) == SelComp) + { + TargetClass = Comp->GetClass(); + break; + } + } + } + + if (!TargetClass) return Names; + + for (TFieldIterator 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; +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h new file mode 100644 index 0000000..4d67375 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_EditableComponent.h @@ -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 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 DisplayNames; + + /** Lists available component names on the owning actor. */ + UFUNCTION() + TArray GetAvailableComponentNames() const; + + /** Lists editable properties for the currently SelectedComponent. */ + UFUNCTION() + TArray 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; +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index bde80aa..22fe3e4 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -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& Weak : SpawnedActors) + { + if (Weak.Get() == Actor) + { + return true; + } + } + return false; +} + TArray UPS_Editor_SpawnManager::GetCategories() const { TArray Categories; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h index 86a7041..9b29f18 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.h @@ -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 OwnerPC; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index 1cf11c8..d4b921f 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -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 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 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 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>& SpawnedActors = SM->GetSpawnedActors(); + int32 VisibleCount = 0; + for (const TWeakObjectPtr& 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(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 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 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(); + 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 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(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 ValueWidget = SNullWidget::NullWidget; + + if (CastField(Row.CachedProperty)) + { + ValueWidget = SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth() + [ + SAssignNew(Row.BoolField, SCheckBox) + .OnCheckStateChanged_Lambda([this, RowIndex](ECheckBoxState) + { + ApplyPropertyFromUI(RowIndex); + }) + ]; + } + else if (FStructProperty* StructProp = CastField(Row.CachedProperty)) + { + auto MakeTextField = [&](TSharedPtr& Field) -> TSharedRef + { + return SAssignNew(Field, SEditableTextBox) + .Font(ValueFont) + .MinDesiredWidth(55.0f) + .OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type) + { + ApplyPropertyFromUI(RowIndex); + }); + }; + + if (StructProp->Struct == TBaseStructure::Get() + || StructProp->Struct == TBaseStructure::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::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(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(Row.CachedProperty)) + { + void* ValuePtr = Row.CachedProperty->ContainerPtrToValuePtr(Target); + if (!ValuePtr) continue; + + auto SetIfNoFocus = [this](TSharedPtr& Field, float Val) + { + if (Field.IsValid() && !Field->HasKeyboardFocus()) + { + Field->SetText(FText::FromString(FloatToString(Val))); + } + }; + + if (StructProp->Struct == TBaseStructure::Get()) + { + const FVector& Vec = *static_cast(ValuePtr); + SetIfNoFocus(Row.VecX, Vec.X); + SetIfNoFocus(Row.VecY, Vec.Y); + SetIfNoFocus(Row.VecZ, Vec.Z); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + const FRotator& Rot = *static_cast(ValuePtr); + SetIfNoFocus(Row.VecX, Rot.Pitch); + SetIfNoFocus(Row.VecY, Rot.Yaw); + SetIfNoFocus(Row.VecZ, Rot.Roll); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + const FLinearColor& Col = *static_cast(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(Row.CachedProperty)) + { + const float Val = *Row.CachedProperty->ContainerPtrToValuePtr(Target); + Row.SingleField->SetText(FText::FromString(FloatToString(Val))); + } + else if (CastField(Row.CachedProperty)) + { + const double Val = *Row.CachedProperty->ContainerPtrToValuePtr(Target); + Row.SingleField->SetText(FText::FromString(FloatToString(static_cast(Val)))); + } + else if (CastField(Row.CachedProperty)) + { + const int32 Val = *Row.CachedProperty->ContainerPtrToValuePtr(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(Row.CachedProperty)) + { + NewValueText = Row.BoolField.IsValid() && Row.BoolField->IsChecked() ? TEXT("True") : TEXT("False"); + } + else if (FStructProperty* StructProp = CastField(Row.CachedProperty)) + { + auto GetText = [](TSharedPtr& F) -> FString + { + return F.IsValid() ? F->GetText().ToString() : TEXT("0"); + }; + + if (StructProp->Struct == TBaseStructure::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::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::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(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(PC)) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + 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); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 4329a09..59abe9d 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -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 SingleField; // float, double, int32, FString + TSharedPtr BoolField; // bool + TSharedPtr VecX, VecY, VecZ; // FVector, FRotator + TSharedPtr 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 RotX, RotY, RotZ; TSharedPtr ScaX, ScaY, ScaZ; + // Dynamic property editing + TSharedPtr DynamicPropertiesContainer; + TArray DynamicPropertyRows; + TWeakObjectPtr LastSelectedActor; + + // Outliner + TSharedPtr OutlinerListContainer; + int32 LastOutlinerActorCount = -1; + // ---- Helpers ---- TSharedRef BuildToolbar(); TSharedRef BuildPropertiesPanel(); TSharedRef BuildSpawnCatalog(); TSharedRef BuildSceneButtons(); + TSharedRef 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);