Close button, OnPropertiesLoaded event, spawn raytrace, IsLoadingFromScene
- Add close button (X) in title bar with save confirmation overlay - Add OnEditorClosed delegate on PlayerController (BP-bindable) - Add OnPropertiesLoaded event to EditableInterface (fires after scene load) - Add IsLoadingFromScene() helper for BP BeginPlay skip pattern - ComputeSpawnLocation now raycasts to avoid spawning through floors - Track dirty state for save confirmation on close - Scene load uses PS_Editor_Loading tag via CustomPreSpawnInitalization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,37 @@ APS_Editor_SplineActor* APS_Editor_PlayerController::GetActiveSplineActor() cons
|
|||||||
return Cast<APS_Editor_SplineActor>(ActivePlacementActor);
|
return Cast<APS_Editor_SplineActor>(ActivePlacementActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APS_Editor_PlayerController::CloseEditor()
|
||||||
|
{
|
||||||
|
// Cancel active placement
|
||||||
|
if (EditorMode == EPS_Editor_EditorMode::SplinePlacement)
|
||||||
|
{
|
||||||
|
CancelPointPlacement();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
if (SelectionManager)
|
||||||
|
{
|
||||||
|
SelectionManager->ClearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify actors: leaving editor mode
|
||||||
|
if (SpawnManager)
|
||||||
|
{
|
||||||
|
SpawnManager->NotifyEditorModeChanged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload sublevel
|
||||||
|
LoadBaseLevelAsSublevel(TEXT("None"));
|
||||||
|
|
||||||
|
// Reset input
|
||||||
|
bShowMouseCursor = false;
|
||||||
|
SetInputMode(FInputModeGameOnly());
|
||||||
|
|
||||||
|
// Broadcast delegate — the game handles the transition
|
||||||
|
OnEditorClosed.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
void APS_Editor_PlayerController::BeginPointPlacement(TSubclassOf<AActor> InActorClass)
|
||||||
{
|
{
|
||||||
if (SelectionManager)
|
if (SelectionManager)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class APS_Editor_SplineActor;
|
|||||||
class IPS_Editor_PointPlaceable;
|
class IPS_Editor_PointPlaceable;
|
||||||
class ULevelStreamingDynamic;
|
class ULevelStreamingDynamic;
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEditorClosed);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player controller for PS_Editor runtime editing.
|
* Player controller for PS_Editor runtime editing.
|
||||||
* Owns the SelectionManager and wires it to the Pawn's click delegate.
|
* Owns the SelectionManager and wires it to the Pawn's click delegate.
|
||||||
@@ -47,6 +49,16 @@ public:
|
|||||||
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
UPROPERTY(BlueprintReadWrite, Category = "PS Editor")
|
||||||
FString CurrentBaseLevel;
|
FString CurrentBaseLevel;
|
||||||
|
|
||||||
|
/** Fired when the editor Close button is pressed (after optional save).
|
||||||
|
* Bind this in Blueprint to override the default behavior (quit application).
|
||||||
|
* If nothing is bound, the application exits. */
|
||||||
|
UPROPERTY(BlueprintAssignable, Category = "PS Editor")
|
||||||
|
FOnEditorClosed OnEditorClosed;
|
||||||
|
|
||||||
|
/** Call to close the editor. Broadcasts OnEditorClosed; if unbound, quits the app. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
|
void CloseEditor();
|
||||||
|
|
||||||
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
|
/** Load a base level as sublevel for visual context. Unloads previous sublevel.
|
||||||
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
|
* @param LevelName Display name stored in CurrentBaseLevel (used for scene filtering).
|
||||||
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
|
* @param MapPath Full package path for LoadLevelInstance (e.g. "/Game/Maps/Warehouse").
|
||||||
|
|||||||
@@ -118,9 +118,13 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn actor
|
// Spawn with tag set BEFORE BeginPlay so the BP can skip its default init
|
||||||
FActorSpawnParameters SpawnParams;
|
FActorSpawnParameters SpawnParams;
|
||||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||||
|
SpawnParams.CustomPreSpawnInitalization = [](AActor* Actor)
|
||||||
|
{
|
||||||
|
Actor->Tags.Add(FName("PS_Editor_Loading"));
|
||||||
|
};
|
||||||
|
|
||||||
AActor* SpawnedActor = World->SpawnActor<AActor>(LoadedClass, ActorData.Location, ActorData.Rotation, SpawnParams);
|
AActor* SpawnedActor = World->SpawnActor<AActor>(LoadedClass, ActorData.Location, ActorData.Rotation, SpawnParams);
|
||||||
if (!SpawnedActor)
|
if (!SpawnedActor)
|
||||||
@@ -148,10 +152,13 @@ AActor* UPS_Editor_SceneLoader::SpawnActorFromData(UWorld* World, const FPS_Edit
|
|||||||
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
Spline->ImportFromCustomProperties(ActorData.CustomProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the actor so it can re-initialize with the loaded property values
|
// Remove loading tag now that properties are applied
|
||||||
|
SpawnedActor->Tags.Remove(FName("PS_Editor_Loading"));
|
||||||
|
|
||||||
|
// Notify the actor that all properties are ready
|
||||||
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||||
{
|
{
|
||||||
IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(SpawnedActor);
|
IPS_Editor_EditableInterface::Execute_OnPropertiesLoaded(SpawnedActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SpawnedActor;
|
return SpawnedActor;
|
||||||
@@ -202,6 +209,11 @@ void UPS_Editor_SceneLoader::ApplyCustomProperties(AActor* Actor, const TMap<FSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UPS_Editor_SceneLoader::IsLoadingFromScene(const AActor* Actor)
|
||||||
|
{
|
||||||
|
return Actor && Actor->ActorHasTag(FName("PS_Editor_Loading"));
|
||||||
|
}
|
||||||
|
|
||||||
FString UPS_Editor_SceneLoader::GetScenesDirectory()
|
FString UPS_Editor_SceneLoader::GetScenesDirectory()
|
||||||
{
|
{
|
||||||
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PS_Editor"), TEXT("Scenes"));
|
return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("PS_Editor"), TEXT("Scenes"));
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
static FString GetSceneLevelName(const FString& SceneName);
|
static FString GetSceneLevelName(const FString& SceneName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this actor is currently being spawned from a scene load.
|
||||||
|
* Use in BeginPlay to skip default initialization — wait for OnPropertiesLoaded instead.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PS Editor")
|
||||||
|
static bool IsLoadingFromScene(const AActor* Actor);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all saved scenes for the current level.
|
* Get all saved scenes for the current level.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
|
UClass* LoadedClass = LoadObject<UClass>(nullptr, *ActorData.ClassPath);
|
||||||
if (!LoadedClass)
|
if (!LoadedClass)
|
||||||
{
|
{
|
||||||
// Fallback for C++ native classes (e.g. /Script/PS_Editor.PS_Editor_SplineActor)
|
|
||||||
LoadedClass = FindObject<UClass>(nullptr, *ActorData.ClassPath);
|
LoadedClass = FindObject<UClass>(nullptr, *ActorData.ClassPath);
|
||||||
}
|
}
|
||||||
if (!LoadedClass)
|
if (!LoadedClass)
|
||||||
@@ -221,7 +220,7 @@ bool UPS_Editor_SceneSerializer::LoadScene(const FString& SceneName, UPS_Editor_
|
|||||||
// Notify the actor that properties were loaded and we are in editor mode
|
// Notify the actor that properties were loaded and we are in editor mode
|
||||||
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
if (SpawnedActor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()))
|
||||||
{
|
{
|
||||||
IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(SpawnedActor);
|
IPS_Editor_EditableInterface::Execute_OnPropertiesLoaded(SpawnedActor);
|
||||||
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
|
IPS_Editor_EditableInterface::Execute_OnEditorModeChanged(SpawnedActor, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ public:
|
|||||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor")
|
||||||
void OnEditorNeedsRespawn();
|
void OnEditorNeedsRespawn();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after all properties have been applied from a scene load (JSON).
|
||||||
|
* Fires both in editor (SceneSerializer) and in gameplay (SceneLoader).
|
||||||
|
* Use this to re-initialize your actor based on the loaded property values.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "PS Editor")
|
||||||
|
void OnPropertiesLoaded();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the actor enters or exits editor mode.
|
* Called when the actor enters or exits editor mode.
|
||||||
* Use this to show/hide editor-only helpers (VR guides, placement aids, debug visuals, etc.).
|
* Use this to show/hide editor-only helpers (VR guides, placement aids, debug visuals, etc.).
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
|
|||||||
FVector UPS_Editor_SpawnManager::ComputeSpawnLocation() const
|
FVector UPS_Editor_SpawnManager::ComputeSpawnLocation() const
|
||||||
{
|
{
|
||||||
APlayerController* PC = OwnerPC.Get();
|
APlayerController* PC = OwnerPC.Get();
|
||||||
if (!PC)
|
if (!PC || !PC->GetWorld())
|
||||||
{
|
{
|
||||||
return FVector::ZeroVector;
|
return FVector::ZeroVector;
|
||||||
}
|
}
|
||||||
@@ -182,8 +182,23 @@ FVector UPS_Editor_SpawnManager::ComputeSpawnLocation() const
|
|||||||
FRotator CameraRotation;
|
FRotator CameraRotation;
|
||||||
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||||
|
|
||||||
// Spawn 500 units in front of the camera
|
const FVector Direction = CameraRotation.Vector();
|
||||||
return CameraLocation + CameraRotation.Vector() * 500.0f;
|
const float MaxDistance = 5000.0f;
|
||||||
|
const float DefaultDistance = 500.0f;
|
||||||
|
const float SurfaceOffset = 5.0f;
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
FCollisionQueryParams Params;
|
||||||
|
Params.AddIgnoredActor(PC->GetPawn());
|
||||||
|
|
||||||
|
if (PC->GetWorld()->LineTraceSingleByChannel(Hit, CameraLocation, CameraLocation + Direction * MaxDistance, ECC_Visibility, Params))
|
||||||
|
{
|
||||||
|
// Hit something — place at impact point, slightly offset along the surface normal
|
||||||
|
return Hit.ImpactPoint + Hit.ImpactNormal * SurfaceOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing hit — fallback to default distance
|
||||||
|
return CameraLocation + Direction * DefaultDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale)
|
AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale)
|
||||||
|
|||||||
@@ -81,6 +81,29 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
|
||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||||
]
|
]
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.FillWidth(1.0f)
|
||||||
|
[
|
||||||
|
SNullWidget::NullWidget
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.AutoWidth()
|
||||||
|
.VAlign(VAlign_Center)
|
||||||
|
.Padding(8.0f, 0.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
OnCloseClicked();
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("X")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 12))
|
||||||
|
.ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.3f))
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
// Toolbar
|
// Toolbar
|
||||||
@@ -220,6 +243,76 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
// Close confirmation overlay
|
||||||
|
+ SOverlay::Slot()
|
||||||
|
.HAlign(HAlign_Center)
|
||||||
|
.VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SAssignNew(CloseConfirmOverlay, SBorder)
|
||||||
|
.Visibility(EVisibility::Collapsed)
|
||||||
|
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
|
||||||
|
.BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f))
|
||||||
|
.Padding(FMargin(32.0f, 20.0f))
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Save changes before closing?")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 13))
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
ExecuteClose(true);
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Save & Close")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
ExecuteClose(false);
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Close without saving")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||||
|
.ColorAndOpacity(FLinearColor(1.0f, 0.5f, 0.3f))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
if (CloseConfirmOverlay.IsValid())
|
||||||
|
CloseConfirmOverlay->SetVisibility(EVisibility::Collapsed);
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Cancel")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,6 +579,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
if (SaveSM)
|
if (SaveSM)
|
||||||
{
|
{
|
||||||
SceneSerializer->SaveScene(Name, SaveSM);
|
SceneSerializer->SaveScene(Name, SaveSM);
|
||||||
|
bSceneDirty = false;
|
||||||
PopulateSceneList();
|
PopulateSceneList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,6 +624,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
if (!FileName.IsEmpty() && SceneSerializer.IsValid() && SpawnManager.IsValid())
|
if (!FileName.IsEmpty() && SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||||
{
|
{
|
||||||
SceneSerializer->SaveScene(FileName, SpawnManager.Get());
|
SceneSerializer->SaveScene(FileName, SpawnManager.Get());
|
||||||
|
bSceneDirty = false;
|
||||||
if (SaveNameField.IsValid())
|
if (SaveNameField.IsValid())
|
||||||
{
|
{
|
||||||
SaveNameField->SetText(FText::FromString(FileName));
|
SaveNameField->SetText(FText::FromString(FileName));
|
||||||
@@ -562,6 +657,8 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
if (SceneSerializer.IsValid() && SpawnManager.IsValid())
|
||||||
{
|
{
|
||||||
SceneSerializer->ClearScene(SpawnManager.Get());
|
SceneSerializer->ClearScene(SpawnManager.Get());
|
||||||
|
bSceneDirty = false;
|
||||||
|
LastKnownSpawnedCount = 0;
|
||||||
}
|
}
|
||||||
return FReply::Handled();
|
return FReply::Handled();
|
||||||
})
|
})
|
||||||
@@ -713,6 +810,7 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::BuildSceneButtons()
|
|||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer->LoadScene(FileName, SpawnManager.Get());
|
SceneSerializer->LoadScene(FileName, SpawnManager.Get());
|
||||||
|
bSceneDirty = false;
|
||||||
if (SaveNameField.IsValid())
|
if (SaveNameField.IsValid())
|
||||||
{
|
{
|
||||||
SaveNameField->SetText(FText::FromString(FileName));
|
SaveNameField->SetText(FText::FromString(FileName));
|
||||||
@@ -969,6 +1067,7 @@ void UPS_Editor_MainWidget::PopulateSceneList()
|
|||||||
}
|
}
|
||||||
|
|
||||||
SceneSerializer->LoadScene(Name, SpawnManager.Get());
|
SceneSerializer->LoadScene(Name, SpawnManager.Get());
|
||||||
|
bSceneDirty = false;
|
||||||
if (SaveNameField.IsValid())
|
if (SaveNameField.IsValid())
|
||||||
{
|
{
|
||||||
SaveNameField->SetText(FText::FromString(Name));
|
SaveNameField->SetText(FText::FromString(Name));
|
||||||
@@ -1140,6 +1239,7 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mgr->SpawnFromCatalog(EntryIndex);
|
Mgr->SpawnFromCatalog(EntryIndex);
|
||||||
|
bSceneDirty = true;
|
||||||
return FReply::Handled();
|
return FReply::Handled();
|
||||||
})
|
})
|
||||||
[
|
[
|
||||||
@@ -1157,6 +1257,21 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog()
|
|||||||
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||||
{
|
{
|
||||||
Super::NativeTick(MyGeometry, InDeltaTime);
|
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||||
|
|
||||||
|
// Track dirty state: detect spawned actor count changes (spawn, delete, undo, paste)
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager())
|
||||||
|
{
|
||||||
|
int32 CurrentCount = SM->GetSpawnedActors().Num();
|
||||||
|
if (CurrentCount != LastKnownSpawnedCount)
|
||||||
|
{
|
||||||
|
if (CurrentCount > 0) bSceneDirty = true;
|
||||||
|
LastKnownSpawnedCount = CurrentCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RefreshPropertiesFromSelection();
|
RefreshPropertiesFromSelection();
|
||||||
RefreshOutliner();
|
RefreshOutliner();
|
||||||
}
|
}
|
||||||
@@ -1347,6 +1462,7 @@ void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
|||||||
void UPS_Editor_MainWidget::ApplyTransformFromUI()
|
void UPS_Editor_MainWidget::ApplyTransformFromUI()
|
||||||
{
|
{
|
||||||
if (bIsRefreshing) return;
|
if (bIsRefreshing) return;
|
||||||
|
bSceneDirty = true;
|
||||||
|
|
||||||
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||||
if (!SM || !SM->IsAnythingSelected()) return;
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
@@ -1886,6 +2002,7 @@ void UPS_Editor_MainWidget::RefreshDynamicPropertyValues()
|
|||||||
void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
void UPS_Editor_MainWidget::ApplyPropertyFromUI(int32 RowIndex)
|
||||||
{
|
{
|
||||||
if (bIsRefreshing) return;
|
if (bIsRefreshing) return;
|
||||||
|
bSceneDirty = true;
|
||||||
if (!DynamicPropertyRows.IsValidIndex(RowIndex)) return;
|
if (!DynamicPropertyRows.IsValidIndex(RowIndex)) return;
|
||||||
|
|
||||||
FPS_Editor_PropertyRowWidgets& Row = DynamicPropertyRows[RowIndex];
|
FPS_Editor_PropertyRowWidgets& Row = DynamicPropertyRows[RowIndex];
|
||||||
@@ -2066,3 +2183,46 @@ void UPS_Editor_MainWidget::OnModeButtonClicked(EPS_Editor_TransformMode Mode)
|
|||||||
SM->SetTransformMode(Mode);
|
SM->SetTransformMode(Mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OnCloseClicked()
|
||||||
|
{
|
||||||
|
if (CloseConfirmOverlay.IsValid())
|
||||||
|
{
|
||||||
|
CloseConfirmOverlay->SetVisibility(EVisibility::Visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::ExecuteClose(bool bSave)
|
||||||
|
{
|
||||||
|
if (CloseConfirmOverlay.IsValid())
|
||||||
|
{
|
||||||
|
CloseConfirmOverlay->SetVisibility(EVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetOwningPlayer());
|
||||||
|
if (!EditorPC) return;
|
||||||
|
|
||||||
|
if (bSave)
|
||||||
|
{
|
||||||
|
UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager();
|
||||||
|
UPS_Editor_SceneSerializer* Ser = EditorPC->GetSceneSerializer();
|
||||||
|
if (SM && Ser)
|
||||||
|
{
|
||||||
|
FString SceneName = SaveNameField.IsValid() ? SaveNameField->GetText().ToString() : TEXT("");
|
||||||
|
if (SceneName.IsEmpty())
|
||||||
|
{
|
||||||
|
if (HelpBarText.IsValid())
|
||||||
|
{
|
||||||
|
HelpBarText->SetText(FText::FromString(TEXT("Enter a scenario name before saving.")));
|
||||||
|
HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.2f));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ser->SaveScene(SceneName, SM);
|
||||||
|
bSceneDirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate cleanup + transition to PlayerController
|
||||||
|
EditorPC->CloseEditor();
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ private:
|
|||||||
TSharedPtr<SWidget> SplineEditButton;
|
TSharedPtr<SWidget> SplineEditButton;
|
||||||
TSharedPtr<SWidget> SplineDeletePointButton;
|
TSharedPtr<SWidget> SplineDeletePointButton;
|
||||||
|
|
||||||
|
// Close confirmation
|
||||||
|
TSharedPtr<SWidget> CloseConfirmOverlay;
|
||||||
|
bool bSceneDirty = false;
|
||||||
|
int32 LastKnownSpawnedCount = 0;
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
TSharedRef<SWidget> BuildToolbar();
|
TSharedRef<SWidget> BuildToolbar();
|
||||||
TSharedRef<SWidget> BuildPropertiesPanel();
|
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||||
@@ -123,6 +128,8 @@ private:
|
|||||||
FString FloatToString(float Value) const;
|
FString FloatToString(float Value) const;
|
||||||
|
|
||||||
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
|
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
|
||||||
|
void OnCloseClicked();
|
||||||
|
void ExecuteClose(bool bSave);
|
||||||
|
|
||||||
/** Whether we are currently refreshing UI from selection (avoid feedback loop). */
|
/** Whether we are currently refreshing UI from selection (avoid feedback loop). */
|
||||||
bool bIsRefreshing = false;
|
bool bIsRefreshing = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user