Add undo/redo system, properties panel, and toolbar UI
Undo/Redo: - Custom runtime UndoManager (UE transaction system is editor-only) - Ctrl+Z / Ctrl+Y hotkeys, action stack with 50 history limit - Records: gizmo translate/rotate/scale, snap to ground, UI edits - Soft-delete on Delete key (hide actor, undo restores it) - Gizmo follows restored position on undo/redo UI: - Properties panel (right side): editable Location/Rotation/Scale fields - Real-time updates during gizmo drag, manual input with Enter to apply - Toolbar: Translate(E)/Rotate(R)/Scale(T) buttons + Snap Ground - Mode indicator in title bar, updated help text with all shortcuts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "DrawDebugHelpers.h"
|
#include "DrawDebugHelpers.h"
|
||||||
|
|
||||||
APS_Editor_Pawn::APS_Editor_Pawn()
|
APS_Editor_Pawn::APS_Editor_Pawn()
|
||||||
@@ -172,6 +173,15 @@ void APS_Editor_Pawn::CreateInputActions()
|
|||||||
IA_SnapToGround = NewObject<UInputAction>(this, TEXT("IA_SnapToGround"));
|
IA_SnapToGround = NewObject<UInputAction>(this, TEXT("IA_SnapToGround"));
|
||||||
IA_SnapToGround->ValueType = EInputActionValueType::Boolean;
|
IA_SnapToGround->ValueType = EInputActionValueType::Boolean;
|
||||||
EditorMappingContext->MapKey(IA_SnapToGround, EKeys::End);
|
EditorMappingContext->MapKey(IA_SnapToGround, EKeys::End);
|
||||||
|
|
||||||
|
// Undo/Redo (Ctrl+Z / Ctrl+Y handled via key state check in handler)
|
||||||
|
IA_Undo = NewObject<UInputAction>(this, TEXT("IA_Undo"));
|
||||||
|
IA_Undo->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Undo, EKeys::Z);
|
||||||
|
|
||||||
|
IA_Redo = NewObject<UInputAction>(this, TEXT("IA_Redo"));
|
||||||
|
IA_Redo->ValueType = EInputActionValueType::Boolean;
|
||||||
|
EditorMappingContext->MapKey(IA_Redo, EKeys::Y);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||||
@@ -210,6 +220,8 @@ void APS_Editor_Pawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComp
|
|||||||
EIC->BindAction(IA_ScaleMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleScaleMode);
|
EIC->BindAction(IA_ScaleMode, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleScaleMode);
|
||||||
EIC->BindAction(IA_Delete, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDelete);
|
EIC->BindAction(IA_Delete, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDelete);
|
||||||
EIC->BindAction(IA_SnapToGround, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSnapToGround);
|
EIC->BindAction(IA_SnapToGround, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleSnapToGround);
|
||||||
|
EIC->BindAction(IA_Undo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleUndo);
|
||||||
|
EIC->BindAction(IA_Redo, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleRedo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Input Handlers ----
|
// ---- Input Handlers ----
|
||||||
@@ -440,8 +452,48 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
|||||||
OnEditorClick.ExecuteIfBound(PendingClickScreenPos, bCtrlHeld);
|
OnEditorClick.ExecuteIfBound(PendingClickScreenPos, bCtrlHeld);
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
}
|
}
|
||||||
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit
|
else if (CameraMode == EPS_Editor_CameraMode::GizmoDrag)
|
||||||
|| CameraMode == EPS_Editor_CameraMode::GizmoDrag)
|
{
|
||||||
|
// Record undo action for the completed gizmo drag
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
||||||
|
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||||
|
|
||||||
|
for (int32 i = 0; i < Selected.Num() && i < GizmoDragInitialTransforms.Num(); ++i)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Selected[i].Get())
|
||||||
|
{
|
||||||
|
Action->Actors.Add(Actor);
|
||||||
|
Action->OldTransforms.Add(GizmoDragInitialTransforms[i]);
|
||||||
|
Action->NewTransforms.Add(Actor->GetActorTransform());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ModeStr;
|
||||||
|
switch (SM->GetTransformMode())
|
||||||
|
{
|
||||||
|
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate"); break;
|
||||||
|
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate"); break;
|
||||||
|
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale"); break;
|
||||||
|
}
|
||||||
|
Action->Description = FString::Printf(TEXT("%s %d actor(s)"), *ModeStr, Action->Actors.Num());
|
||||||
|
|
||||||
|
if (Action->Actors.Num() > 0)
|
||||||
|
{
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveGizmoAxis = EPS_Editor_GizmoAxis::None;
|
||||||
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
|
}
|
||||||
|
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit)
|
||||||
{
|
{
|
||||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||||
}
|
}
|
||||||
@@ -516,20 +568,52 @@ void APS_Editor_Pawn::HandleScaleMode(const FInputActionValue& Value)
|
|||||||
void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||||
{
|
{
|
||||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||||
{
|
if (!EditorPC) return;
|
||||||
if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager())
|
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||||
{
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
TArray<TWeakObjectPtr<AActor>> ToDelete = SM->GetSelectedActors();
|
|
||||||
SM->ClearSelection();
|
// Soft-delete: hide actors instead of destroying (enables undo)
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : ToDelete)
|
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||||
{
|
{
|
||||||
if (AActor* Actor = Weak.Get())
|
if (AActor* Actor = Weak.Get())
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deleted %s"), *Actor->GetName());
|
Action->Actors.Add(Actor);
|
||||||
Actor->Destroy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SM->ClearSelection();
|
||||||
|
Action->Execute(); // Hide the actors
|
||||||
|
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Soft-deleted %d actor(s)"), Action->Actors.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleUndo(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
UM->Undo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_Editor_Pawn::HandleRedo(const FInputActionValue& Value)
|
||||||
|
{
|
||||||
|
if (!bCtrlHeld || CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController()))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
UM->Redo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,12 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UInputAction> IA_SnapToGround;
|
TObjectPtr<UInputAction> IA_SnapToGround;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Undo;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UInputAction> IA_Redo;
|
||||||
|
|
||||||
// ---- Modifier key state ----
|
// ---- Modifier key state ----
|
||||||
bool bAltHeld = false;
|
bool bAltHeld = false;
|
||||||
bool bCtrlHeld = false;
|
bool bCtrlHeld = false;
|
||||||
@@ -191,6 +197,8 @@ private:
|
|||||||
void HandleScaleMode(const FInputActionValue& Value);
|
void HandleScaleMode(const FInputActionValue& Value);
|
||||||
void HandleDelete(const FInputActionValue& Value);
|
void HandleDelete(const FInputActionValue& Value);
|
||||||
void HandleSnapToGround(const FInputActionValue& Value);
|
void HandleSnapToGround(const FInputActionValue& Value);
|
||||||
|
void HandleUndo(const FInputActionValue& Value);
|
||||||
|
void HandleRedo(const FInputActionValue& Value);
|
||||||
|
|
||||||
// ---- Helpers ----
|
// ---- Helpers ----
|
||||||
void CreateInputActions();
|
void CreateInputActions();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "PS_Editor_PlayerController.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "PS_Editor_Gizmo.h"
|
||||||
#include "PS_Editor_Pawn.h"
|
#include "PS_Editor_Pawn.h"
|
||||||
|
|
||||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||||
@@ -17,6 +19,20 @@ void APS_Editor_PlayerController::BeginPlay()
|
|||||||
// Create selection manager
|
// Create selection manager
|
||||||
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
|
SelectionManager = NewObject<UPS_Editor_SelectionManager>(this);
|
||||||
SelectionManager->Initialize(this);
|
SelectionManager->Initialize(this);
|
||||||
|
|
||||||
|
UndoManager = NewObject<UPS_Editor_UndoManager>(this);
|
||||||
|
|
||||||
|
// After undo/redo, refresh the gizmo position
|
||||||
|
UndoManager->OnUndoRedo.AddWeakLambda(this, [this]()
|
||||||
|
{
|
||||||
|
if (SelectionManager && SelectionManager->IsAnythingSelected())
|
||||||
|
{
|
||||||
|
if (APS_Editor_Gizmo* Gizmo = SelectionManager->GetGizmo())
|
||||||
|
{
|
||||||
|
Gizmo->SetActorLocation(SelectionManager->GetSelectionPivot());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "PS_Editor_PlayerController.generated.h"
|
#include "PS_Editor_PlayerController.generated.h"
|
||||||
|
|
||||||
class UPS_Editor_SelectionManager;
|
class UPS_Editor_SelectionManager;
|
||||||
|
class UPS_Editor_UndoManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player controller for PS_Editor runtime editing.
|
* Player controller for PS_Editor runtime editing.
|
||||||
@@ -21,6 +22,9 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||||
|
UPS_Editor_UndoManager* GetUndoManager() const { return UndoManager; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void BeginPlay() override;
|
virtual void BeginPlay() override;
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
@@ -29,5 +33,8 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
TObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UPS_Editor_UndoManager> UndoManager;
|
||||||
|
|
||||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "PS_Editor_SelectionManager.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "PS_Editor_Gizmo.h"
|
#include "PS_Editor_Gizmo.h"
|
||||||
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "GameFramework/Pawn.h"
|
#include "GameFramework/Pawn.h"
|
||||||
@@ -223,17 +225,22 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
|||||||
APlayerController* PC = OwnerPC.Get();
|
APlayerController* PC = OwnerPC.Get();
|
||||||
if (!PC) return;
|
if (!PC) return;
|
||||||
|
|
||||||
|
// Capture transforms BEFORE snap for undo
|
||||||
|
TSharedPtr<FPS_Editor_TransformAction> UndoAction = MakeShared<FPS_Editor_TransformAction>();
|
||||||
|
UndoAction->Description = FString::Printf(TEXT("Snap to ground %d actor(s)"), SelectedActors.Num());
|
||||||
|
|
||||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||||
{
|
{
|
||||||
AActor* Actor = Weak.Get();
|
AActor* Actor = Weak.Get();
|
||||||
if (!Actor) continue;
|
if (!Actor) continue;
|
||||||
|
|
||||||
// Get actor bounds to compute the bottom of the actor
|
UndoAction->Actors.Add(Actor);
|
||||||
|
UndoAction->OldTransforms.Add(Actor->GetActorTransform());
|
||||||
|
|
||||||
FVector Origin, Extent;
|
FVector Origin, Extent;
|
||||||
Actor->GetActorBounds(false, Origin, Extent);
|
Actor->GetActorBounds(false, Origin, Extent);
|
||||||
const float BottomOffset = Extent.Z; // Distance from actor origin to bottom
|
const float BottomOffset = Extent.Z;
|
||||||
|
|
||||||
// Trace downward from actor location
|
|
||||||
const FVector Start = Actor->GetActorLocation();
|
const FVector Start = Actor->GetActorLocation();
|
||||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
const FVector End = Start - FVector::UpVector * 100000.0f;
|
||||||
|
|
||||||
@@ -245,12 +252,23 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
|||||||
|
|
||||||
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params))
|
||||||
{
|
{
|
||||||
// Place actor so its bottom sits on the ground
|
|
||||||
FVector NewLocation = Hit.ImpactPoint;
|
FVector NewLocation = Hit.ImpactPoint;
|
||||||
NewLocation.Z += BottomOffset;
|
NewLocation.Z += BottomOffset;
|
||||||
Actor->SetActorLocation(NewLocation);
|
Actor->SetActorLocation(NewLocation);
|
||||||
|
}
|
||||||
|
|
||||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Snapped %s to ground"), *Actor->GetName());
|
UndoAction->NewTransforms.Add(Actor->GetActorTransform());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record undo action
|
||||||
|
if (UndoAction->Actors.Num() > 0)
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
UM->RecordAction(UndoAction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#include "PS_Editor_UndoManager.h"
|
||||||
|
|
||||||
|
void UPS_Editor_UndoManager::RecordAction(TSharedPtr<FPS_Editor_Action> Action)
|
||||||
|
{
|
||||||
|
if (!Action.IsValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard any redo history (actions after CurrentIndex)
|
||||||
|
if (CurrentIndex < ActionStack.Num() - 1)
|
||||||
|
{
|
||||||
|
// Cleanup delete actions that will be discarded (truly destroy hidden actors)
|
||||||
|
for (int32 i = CurrentIndex + 1; i < ActionStack.Num(); ++i)
|
||||||
|
{
|
||||||
|
CleanupDeleteAction(ActionStack[i]);
|
||||||
|
}
|
||||||
|
ActionStack.SetNum(CurrentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionStack.Add(Action);
|
||||||
|
CurrentIndex = ActionStack.Num() - 1;
|
||||||
|
|
||||||
|
PruneHistory();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Recorded action: %s (stack size: %d)"),
|
||||||
|
*Action->GetDescription(), ActionStack.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_UndoManager::Undo()
|
||||||
|
{
|
||||||
|
if (!CanUndo())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedPtr<FPS_Editor_Action>& Action = ActionStack[CurrentIndex];
|
||||||
|
Action->Undo();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Undo: %s"), *Action->GetDescription());
|
||||||
|
|
||||||
|
CurrentIndex--;
|
||||||
|
OnUndoRedo.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_UndoManager::Redo()
|
||||||
|
{
|
||||||
|
if (!CanRedo())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentIndex++;
|
||||||
|
TSharedPtr<FPS_Editor_Action>& Action = ActionStack[CurrentIndex];
|
||||||
|
Action->Execute();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Redo: %s"), *Action->GetDescription());
|
||||||
|
OnUndoRedo.Broadcast();
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_UndoManager::GetUndoDescription() const
|
||||||
|
{
|
||||||
|
if (!CanUndo())
|
||||||
|
{
|
||||||
|
return TEXT("");
|
||||||
|
}
|
||||||
|
return ActionStack[CurrentIndex]->GetDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_UndoManager::GetRedoDescription() const
|
||||||
|
{
|
||||||
|
if (!CanRedo())
|
||||||
|
{
|
||||||
|
return TEXT("");
|
||||||
|
}
|
||||||
|
return ActionStack[CurrentIndex + 1]->GetDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_UndoManager::PruneHistory()
|
||||||
|
{
|
||||||
|
while (ActionStack.Num() > MaxHistorySize)
|
||||||
|
{
|
||||||
|
// Cleanup the oldest action before removing
|
||||||
|
CleanupDeleteAction(ActionStack[0]);
|
||||||
|
ActionStack.RemoveAt(0);
|
||||||
|
CurrentIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_UndoManager::CleanupDeleteAction(TSharedPtr<FPS_Editor_Action> Action)
|
||||||
|
{
|
||||||
|
// If a delete action is being pruned from history, truly destroy the hidden actors
|
||||||
|
if (!Action.IsValid() || Action->ActionType != EPS_Editor_ActionType::Delete) return;
|
||||||
|
FPS_Editor_DeleteAction* DeleteAction = static_cast<FPS_Editor_DeleteAction*>(Action.Get());
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : DeleteAction->Actors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
if (Actor->IsHidden())
|
||||||
|
{
|
||||||
|
Actor->Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/NoExportTypes.h"
|
||||||
|
#include "PS_Editor_UndoManager.generated.h"
|
||||||
|
|
||||||
|
enum class EPS_Editor_ActionType : uint8
|
||||||
|
{
|
||||||
|
Transform,
|
||||||
|
Delete
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for undoable editor actions.
|
||||||
|
*/
|
||||||
|
struct FPS_Editor_Action
|
||||||
|
{
|
||||||
|
EPS_Editor_ActionType ActionType;
|
||||||
|
|
||||||
|
explicit FPS_Editor_Action(EPS_Editor_ActionType InType) : ActionType(InType) {}
|
||||||
|
virtual ~FPS_Editor_Action() = default;
|
||||||
|
virtual void Execute() = 0;
|
||||||
|
virtual void Undo() = 0;
|
||||||
|
virtual FString GetDescription() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a transform change on one or more actors.
|
||||||
|
* Used for: gizmo translate/rotate/scale, snap to ground, UI transform edit.
|
||||||
|
*/
|
||||||
|
struct FPS_Editor_TransformAction : public FPS_Editor_Action
|
||||||
|
{
|
||||||
|
FPS_Editor_TransformAction() : FPS_Editor_Action(EPS_Editor_ActionType::Transform) {}
|
||||||
|
|
||||||
|
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||||
|
TArray<FTransform> OldTransforms;
|
||||||
|
TArray<FTransform> NewTransforms;
|
||||||
|
FString Description;
|
||||||
|
|
||||||
|
virtual void Execute() override
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < Actors.Num(); ++i)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Actors[i].Get())
|
||||||
|
{
|
||||||
|
if (i < NewTransforms.Num())
|
||||||
|
{
|
||||||
|
Actor->SetActorTransform(NewTransforms[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Undo() override
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < Actors.Num(); ++i)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Actors[i].Get())
|
||||||
|
{
|
||||||
|
if (i < OldTransforms.Num())
|
||||||
|
{
|
||||||
|
Actor->SetActorTransform(OldTransforms[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override { return Description; }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a soft-delete action (hide + disable collision).
|
||||||
|
* Undo re-shows the actor. Actors are truly destroyed when the action is pruned from the stack.
|
||||||
|
*/
|
||||||
|
struct FPS_Editor_DeleteAction : public FPS_Editor_Action
|
||||||
|
{
|
||||||
|
FPS_Editor_DeleteAction() : FPS_Editor_Action(EPS_Editor_ActionType::Delete) {}
|
||||||
|
|
||||||
|
TArray<TWeakObjectPtr<AActor>> Actors;
|
||||||
|
|
||||||
|
virtual void Execute() override
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
Actor->SetActorHiddenInGame(true);
|
||||||
|
Actor->SetActorEnableCollision(false);
|
||||||
|
Actor->SetActorTickEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Undo() override
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : Actors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
Actor->SetActorHiddenInGame(false);
|
||||||
|
Actor->SetActorEnableCollision(true);
|
||||||
|
Actor->SetActorTickEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("Delete %d actor(s)"), Actors.Num());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime undo/redo manager for PS_Editor.
|
||||||
|
* Maintains a linear action stack with a cursor.
|
||||||
|
* When a new action is recorded after undoing, all redo history is discarded.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class PS_EDITOR_API UPS_Editor_UndoManager : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Record a new undoable action. Discards any redo history. */
|
||||||
|
void RecordAction(TSharedPtr<FPS_Editor_Action> Action);
|
||||||
|
|
||||||
|
/** Undo the last action. */
|
||||||
|
void Undo();
|
||||||
|
|
||||||
|
/** Redo the next action. */
|
||||||
|
void Redo();
|
||||||
|
|
||||||
|
bool CanUndo() const { return CurrentIndex >= 0; }
|
||||||
|
bool CanRedo() const { return CurrentIndex < ActionStack.Num() - 1; }
|
||||||
|
|
||||||
|
/** Get description of the action that would be undone. */
|
||||||
|
FString GetUndoDescription() const;
|
||||||
|
|
||||||
|
/** Get description of the action that would be redone. */
|
||||||
|
FString GetRedoDescription() const;
|
||||||
|
|
||||||
|
/** Called after any undo/redo to update gizmo, UI, etc. */
|
||||||
|
DECLARE_MULTICAST_DELEGATE(FOnUndoRedo);
|
||||||
|
FOnUndoRedo OnUndoRedo;
|
||||||
|
|
||||||
|
/** Maximum number of actions to keep in history. */
|
||||||
|
int32 MaxHistorySize = 50;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TArray<TSharedPtr<FPS_Editor_Action>> ActionStack;
|
||||||
|
|
||||||
|
/** Points to the last executed action. -1 means no actions executed. */
|
||||||
|
int32 CurrentIndex = -1;
|
||||||
|
|
||||||
|
/** Prune oldest actions if stack exceeds MaxHistorySize. */
|
||||||
|
void PruneHistory();
|
||||||
|
|
||||||
|
/** Destroy actors from pruned delete actions. */
|
||||||
|
void CleanupDeleteAction(TSharedPtr<FPS_Editor_Action> Action);
|
||||||
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "PS_Editor_HUD.h"
|
#include "PS_Editor_HUD.h"
|
||||||
#include "PS_Editor_MainWidget.h"
|
#include "PS_Editor_MainWidget.h"
|
||||||
|
#include "PS_Editor_PlayerController.h"
|
||||||
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "Blueprint/UserWidget.h"
|
#include "Blueprint/UserWidget.h"
|
||||||
|
|
||||||
void APS_Editor_HUD::BeginPlay()
|
void APS_Editor_HUD::BeginPlay()
|
||||||
@@ -11,6 +13,12 @@ void APS_Editor_HUD::BeginPlay()
|
|||||||
MainWidget = CreateWidget<UPS_Editor_MainWidget>(PC, UPS_Editor_MainWidget::StaticClass());
|
MainWidget = CreateWidget<UPS_Editor_MainWidget>(PC, UPS_Editor_MainWidget::StaticClass());
|
||||||
if (MainWidget)
|
if (MainWidget)
|
||||||
{
|
{
|
||||||
|
// Wire the SelectionManager to the UI
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
MainWidget->SetSelectionManager(EditorPC->GetSelectionManager());
|
||||||
|
}
|
||||||
|
|
||||||
MainWidget->AddToViewport(0);
|
MainWidget->AddToViewport(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
#include "PS_Editor_MainWidget.h"
|
#include "PS_Editor_MainWidget.h"
|
||||||
#include "Components/TextBlock.h"
|
#include "PS_Editor_SelectionManager.h"
|
||||||
#include "Components/VerticalBox.h"
|
#include "PS_Editor_UndoManager.h"
|
||||||
#include "Components/VerticalBoxSlot.h"
|
#include "PS_Editor_PlayerController.h"
|
||||||
#include "Components/Border.h"
|
|
||||||
#include "Widgets/SBoxPanel.h"
|
#include "Widgets/SBoxPanel.h"
|
||||||
#include "Widgets/Text/STextBlock.h"
|
#include "Widgets/Text/STextBlock.h"
|
||||||
#include "Widgets/Layout/SBorder.h"
|
#include "Widgets/Layout/SBorder.h"
|
||||||
|
#include "Widgets/Layout/SScrollBox.h"
|
||||||
|
#include "Widgets/Input/SButton.h"
|
||||||
|
#include "Widgets/Input/SEditableTextBox.h"
|
||||||
#include "Widgets/SOverlay.h"
|
#include "Widgets/SOverlay.h"
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
||||||
|
{
|
||||||
|
SelectionManager = InSelectionManager;
|
||||||
|
}
|
||||||
|
|
||||||
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||||
{
|
{
|
||||||
// Build the widget tree using Slate (C++ driven UMG)
|
|
||||||
return SNew(SOverlay)
|
return SNew(SOverlay)
|
||||||
// Top bar
|
// Top-left: Title + Toolbar
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
.HAlign(HAlign_Left)
|
.HAlign(HAlign_Left)
|
||||||
.VAlign(VAlign_Top)
|
.VAlign(VAlign_Top)
|
||||||
.Padding(16.0f)
|
.Padding(16.0f)
|
||||||
|
[
|
||||||
|
SNew(SVerticalBox)
|
||||||
|
// Title bar
|
||||||
|
+ SVerticalBox::Slot()
|
||||||
|
.AutoHeight()
|
||||||
[
|
[
|
||||||
SNew(SBorder)
|
SNew(SBorder)
|
||||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||||
@@ -37,13 +48,29 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
.VAlign(VAlign_Center)
|
.VAlign(VAlign_Center)
|
||||||
.Padding(12.0f, 0.0f, 0.0f, 0.0f)
|
.Padding(12.0f, 0.0f, 0.0f, 0.0f)
|
||||||
[
|
[
|
||||||
SNew(STextBlock)
|
SAssignNew(ModeText, STextBlock)
|
||||||
.Text(FText::FromString(TEXT("Edit Mode")))
|
.Text(FText::FromString(TEXT("Translate (E)")))
|
||||||
.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))
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
// Toolbar
|
||||||
|
+ SVerticalBox::Slot()
|
||||||
|
.AutoHeight()
|
||||||
|
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
|
||||||
|
[
|
||||||
|
BuildToolbar()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
// Right panel: Properties
|
||||||
|
+ SOverlay::Slot()
|
||||||
|
.HAlign(HAlign_Right)
|
||||||
|
.VAlign(VAlign_Top)
|
||||||
|
.Padding(16.0f)
|
||||||
|
[
|
||||||
|
BuildPropertiesPanel()
|
||||||
|
]
|
||||||
// Bottom help bar
|
// Bottom help bar
|
||||||
+ SOverlay::Slot()
|
+ SOverlay::Slot()
|
||||||
.HAlign(HAlign_Center)
|
.HAlign(HAlign_Center)
|
||||||
@@ -55,10 +82,163 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
|||||||
.Padding(FMargin(16.0f, 6.0f))
|
.Padding(FMargin(16.0f, 6.0f))
|
||||||
[
|
[
|
||||||
SNew(STextBlock)
|
SNew(STextBlock)
|
||||||
.Text(FText::FromString(TEXT("LMB: Pan | RMB: Fly (ZQSD) | Alt+LMB / MMB: Orbit | E/A: Up/Down | Scroll: Zoom")))
|
.Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | E/R/T: Translate/Rotate/Scale | End: Snap | Del: Delete")))
|
||||||
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
|
.ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildToolbar()
|
||||||
|
{
|
||||||
|
auto MakeButton = [this](const FText& Label, EPS_Editor_TransformMode Mode) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
return SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this, Mode]() -> FReply
|
||||||
|
{
|
||||||
|
OnModeButtonClicked(Mode);
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(Label)
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
|
.Justification(ETextJustify::Center)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return SNew(SBorder)
|
||||||
|
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||||
|
.Padding(FMargin(8.0f, 4.0f))
|
||||||
|
[
|
||||||
|
SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||||
|
[ MakeButton(FText::FromString(TEXT("Translate (E)")), EPS_Editor_TransformMode::Translate) ]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||||
|
[ MakeButton(FText::FromString(TEXT("Rotate (R)")), EPS_Editor_TransformMode::Rotate) ]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f)
|
||||||
|
[ MakeButton(FText::FromString(TEXT("Scale (T)")), EPS_Editor_TransformMode::Scale) ]
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SButton)
|
||||||
|
.OnClicked_Lambda([this]() -> FReply
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||||
|
{
|
||||||
|
SM->SnapSelectionToGround();
|
||||||
|
}
|
||||||
|
return FReply::Handled();
|
||||||
|
})
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Snap Ground")))
|
||||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<SWidget> UPS_Editor_MainWidget::BuildPropertiesPanel()
|
||||||
|
{
|
||||||
|
const FMargin RowPadding(0.0f, 2.0f);
|
||||||
|
const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
|
const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9);
|
||||||
|
const float LabelWidth = 14.0f;
|
||||||
|
const float FieldWidth = 70.0f;
|
||||||
|
|
||||||
|
auto MakeRow = [&](const FText& Label, TSharedPtr<SEditableTextBox>& X, TSharedPtr<SEditableTextBox>& Y, TSharedPtr<SEditableTextBox>& Z, const FLinearColor& ColorX, const FLinearColor& ColorY, const FLinearColor& ColorZ) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
auto MakeField = [&](TSharedPtr<SEditableTextBox>& Field, const FLinearColor& Col) -> TSharedRef<SWidget>
|
||||||
|
{
|
||||||
|
return SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 2.0f, 0.0f)
|
||||||
|
[
|
||||||
|
SNew(SBorder)
|
||||||
|
.BorderBackgroundColor(Col)
|
||||||
|
.Padding(FMargin(3.0f, 1.0f))
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Font(LabelFont)
|
||||||
|
.ColorAndOpacity(FLinearColor::White)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot()
|
||||||
|
.FillWidth(1.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(Field, SEditableTextBox)
|
||||||
|
.Font(ValueFont)
|
||||||
|
.MinDesiredWidth(FieldWidth)
|
||||||
|
.OnTextCommitted_Lambda([this](const FText&, ETextCommit::Type)
|
||||||
|
{
|
||||||
|
ApplyTransformFromUI();
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return SNew(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(Label)
|
||||||
|
.Font(LabelFont)
|
||||||
|
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f))
|
||||||
|
.MinDesiredWidth(LabelWidth)
|
||||||
|
]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||||
|
[ MakeField(X, ColorX) ]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||||
|
[ MakeField(Y, ColorY) ]
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||||
|
[ MakeField(Z, ColorZ) ];
|
||||||
|
};
|
||||||
|
|
||||||
|
const FLinearColor RedX(0.8f, 0.15f, 0.1f);
|
||||||
|
const FLinearColor GreenY(0.15f, 0.6f, 0.1f);
|
||||||
|
const FLinearColor BlueZ(0.1f, 0.3f, 0.8f);
|
||||||
|
|
||||||
|
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(SHorizontalBox)
|
||||||
|
+ SHorizontalBox::Slot().FillWidth(1.0f)
|
||||||
|
[
|
||||||
|
SAssignNew(SelectionInfoText, STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("No selection")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 10))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
|
||||||
|
]
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Location")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||||
|
[ MakeRow(FText::GetEmpty(), LocX, LocY, LocZ, RedX, GreenY, BlueZ) ]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Rotation")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||||
|
[ MakeRow(FText::GetEmpty(), RotX, RotY, RotZ, RedX, GreenY, BlueZ) ]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||||
|
[
|
||||||
|
SNew(STextBlock)
|
||||||
|
.Text(FText::FromString(TEXT("Scale")))
|
||||||
|
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 9))
|
||||||
|
.ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f))
|
||||||
|
]
|
||||||
|
+ SVerticalBox::Slot().AutoHeight().Padding(RowPadding)
|
||||||
|
[ MakeRow(FText::GetEmpty(), ScaX, ScaY, ScaZ, RedX, GreenY, BlueZ) ]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,3 +246,142 @@ void UPS_Editor_MainWidget::NativeConstruct()
|
|||||||
{
|
{
|
||||||
Super::NativeConstruct();
|
Super::NativeConstruct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||||
|
{
|
||||||
|
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||||
|
RefreshPropertiesFromSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::RefreshPropertiesFromSelection()
|
||||||
|
{
|
||||||
|
bIsRefreshing = true;
|
||||||
|
|
||||||
|
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||||
|
if (!SM)
|
||||||
|
{
|
||||||
|
bIsRefreshing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update mode text
|
||||||
|
if (ModeText.IsValid())
|
||||||
|
{
|
||||||
|
FString ModeStr;
|
||||||
|
switch (SM->GetTransformMode())
|
||||||
|
{
|
||||||
|
case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (E)"); break;
|
||||||
|
case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (R)"); break;
|
||||||
|
case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (T)"); break;
|
||||||
|
}
|
||||||
|
ModeText->SetText(FText::FromString(ModeStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SM->IsAnythingSelected())
|
||||||
|
{
|
||||||
|
if (SelectionInfoText.IsValid())
|
||||||
|
{
|
||||||
|
SelectionInfoText->SetText(FText::FromString(TEXT("No selection")));
|
||||||
|
}
|
||||||
|
auto ClearField = [](TSharedPtr<SEditableTextBox>& Field)
|
||||||
|
{
|
||||||
|
if (Field.IsValid()) Field->SetText(FText::FromString(TEXT("--")));
|
||||||
|
};
|
||||||
|
ClearField(LocX); ClearField(LocY); ClearField(LocZ);
|
||||||
|
ClearField(RotX); ClearField(RotY); ClearField(RotZ);
|
||||||
|
ClearField(ScaX); ClearField(ScaY); ClearField(ScaZ);
|
||||||
|
bIsRefreshing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show first selected actor info
|
||||||
|
const TArray<TWeakObjectPtr<AActor>>& Selected = SM->GetSelectedActors();
|
||||||
|
AActor* Actor = Selected[0].Get();
|
||||||
|
if (!Actor)
|
||||||
|
{
|
||||||
|
bIsRefreshing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectionInfoText.IsValid())
|
||||||
|
{
|
||||||
|
FString Info = FString::Printf(TEXT("%s (%d)"), *Actor->GetActorLabel(), Selected.Num());
|
||||||
|
SelectionInfoText->SetText(FText::FromString(Info));
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector Loc = Actor->GetActorLocation();
|
||||||
|
const FRotator Rot = Actor->GetActorRotation();
|
||||||
|
const FVector Sca = Actor->GetActorScale3D();
|
||||||
|
|
||||||
|
auto SetField = [this](TSharedPtr<SEditableTextBox>& Field, float Val)
|
||||||
|
{
|
||||||
|
if (Field.IsValid() && !Field->HasKeyboardFocus())
|
||||||
|
{
|
||||||
|
Field->SetText(FText::FromString(FloatToString(Val)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SetField(LocX, Loc.X); SetField(LocY, Loc.Y); SetField(LocZ, Loc.Z);
|
||||||
|
SetField(RotX, Rot.Roll); SetField(RotY, Rot.Pitch); SetField(RotZ, Rot.Yaw);
|
||||||
|
SetField(ScaX, Sca.X); SetField(ScaY, Sca.Y); SetField(ScaZ, Sca.Z);
|
||||||
|
|
||||||
|
bIsRefreshing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::ApplyTransformFromUI()
|
||||||
|
{
|
||||||
|
if (bIsRefreshing) return;
|
||||||
|
|
||||||
|
UPS_Editor_SelectionManager* SM = SelectionManager.Get();
|
||||||
|
if (!SM || !SM->IsAnythingSelected()) return;
|
||||||
|
|
||||||
|
AActor* Actor = SM->GetSelectedActors()[0].Get();
|
||||||
|
if (!Actor) return;
|
||||||
|
|
||||||
|
auto GetFloat = [](TSharedPtr<SEditableTextBox>& Field, float Default) -> float
|
||||||
|
{
|
||||||
|
if (!Field.IsValid()) return Default;
|
||||||
|
return FCString::Atof(*Field->GetText().ToString());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Capture old transform for undo
|
||||||
|
const FTransform OldTransform = Actor->GetActorTransform();
|
||||||
|
|
||||||
|
const FVector NewLoc(GetFloat(LocX, 0), GetFloat(LocY, 0), GetFloat(LocZ, 0));
|
||||||
|
const FRotator NewRot(GetFloat(RotY, 0), GetFloat(RotZ, 0), GetFloat(RotX, 0));
|
||||||
|
const FVector NewScale(GetFloat(ScaX, 1), GetFloat(ScaY, 1), GetFloat(ScaZ, 1));
|
||||||
|
|
||||||
|
Actor->SetActorLocation(NewLoc);
|
||||||
|
Actor->SetActorRotation(NewRot);
|
||||||
|
Actor->SetActorScale3D(NewScale);
|
||||||
|
|
||||||
|
// Record undo action
|
||||||
|
if (APlayerController* PC = GetOwningPlayer())
|
||||||
|
{
|
||||||
|
if (APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(PC))
|
||||||
|
{
|
||||||
|
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||||
|
{
|
||||||
|
TSharedPtr<FPS_Editor_TransformAction> Action = MakeShared<FPS_Editor_TransformAction>();
|
||||||
|
Action->Actors.Add(Actor);
|
||||||
|
Action->OldTransforms.Add(OldTransform);
|
||||||
|
Action->NewTransforms.Add(Actor->GetActorTransform());
|
||||||
|
Action->Description = TEXT("UI Transform Edit");
|
||||||
|
UM->RecordAction(Action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UPS_Editor_MainWidget::FloatToString(float Value) const
|
||||||
|
{
|
||||||
|
return FString::Printf(TEXT("%.2f"), Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UPS_Editor_MainWidget::OnModeButtonClicked(EPS_Editor_TransformMode Mode)
|
||||||
|
{
|
||||||
|
if (UPS_Editor_SelectionManager* SM = SelectionManager.Get())
|
||||||
|
{
|
||||||
|
SM->SetTransformMode(Mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,24 +2,52 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Blueprint/UserWidget.h"
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "PS_Editor_SelectionTypes.h"
|
||||||
#include "PS_Editor_MainWidget.generated.h"
|
#include "PS_Editor_MainWidget.generated.h"
|
||||||
|
|
||||||
class UTextBlock;
|
class UPS_Editor_SelectionManager;
|
||||||
class UVerticalBox;
|
|
||||||
class UBorder;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main editor widget for PS_Editor.
|
* Main editor widget for PS_Editor.
|
||||||
* Serves as the root container for all editor UI panels.
|
* Contains: title bar, transform mode toolbar, properties panel, help bar.
|
||||||
* Built entirely in C++ (no UMG blueprint).
|
* Built entirely in C++ using Slate.
|
||||||
*/
|
*/
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class PS_EDITOR_API UPS_Editor_MainWidget : public UUserWidget
|
class PS_EDITOR_API UPS_Editor_MainWidget : public UUserWidget
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Set the selection manager reference. Must be called after construction. */
|
||||||
|
void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual TSharedRef<SWidget> RebuildWidget() override;
|
virtual TSharedRef<SWidget> RebuildWidget() override;
|
||||||
|
|
||||||
virtual void NativeConstruct() override;
|
virtual void NativeConstruct() override;
|
||||||
|
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY()
|
||||||
|
TWeakObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||||
|
|
||||||
|
// ---- Slate widget refs for dynamic updates ----
|
||||||
|
TSharedPtr<STextBlock> ModeText;
|
||||||
|
TSharedPtr<STextBlock> SelectionInfoText;
|
||||||
|
|
||||||
|
// Transform fields
|
||||||
|
TSharedPtr<SEditableTextBox> LocX, LocY, LocZ;
|
||||||
|
TSharedPtr<SEditableTextBox> RotX, RotY, RotZ;
|
||||||
|
TSharedPtr<SEditableTextBox> ScaX, ScaY, ScaZ;
|
||||||
|
|
||||||
|
// ---- Helpers ----
|
||||||
|
TSharedRef<SWidget> BuildToolbar();
|
||||||
|
TSharedRef<SWidget> BuildPropertiesPanel();
|
||||||
|
void RefreshPropertiesFromSelection();
|
||||||
|
void ApplyTransformFromUI();
|
||||||
|
FString FloatToString(float Value) const;
|
||||||
|
|
||||||
|
void OnModeButtonClicked(EPS_Editor_TransformMode Mode);
|
||||||
|
|
||||||
|
/** Whether we are currently refreshing UI from selection (avoid feedback loop). */
|
||||||
|
bool bIsRefreshing = false;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user