diff --git a/Unreal/Content/test.umap b/Unreal/Content/test.umap index 1e71822..78cdd07 100644 Binary files a/Unreal/Content/test.umap and b/Unreal/Content/test.umap differ diff --git a/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset b/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset index 6a3523d..b96e120 100644 Binary files a/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset and b/Unreal/Plugins/PS_Editor/Content/Cubeezrzea.uasset differ 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);