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_PlayerController.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
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->ValueType = EInputActionValueType::Boolean;
|
||||
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)
|
||||
@@ -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_Delete, ETriggerEvent::Started, this, &APS_Editor_Pawn::HandleDelete);
|
||||
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 ----
|
||||
@@ -440,8 +452,48 @@ void APS_Editor_Pawn::HandleLeftClickCompleted(const FInputActionValue& Value)
|
||||
OnEditorClick.ExecuteIfBound(PendingClickScreenPos, bCtrlHeld);
|
||||
SetCameraMode(EPS_Editor_CameraMode::Idle);
|
||||
}
|
||||
else if (CameraMode == EPS_Editor_CameraMode::Pan || CameraMode == EPS_Editor_CameraMode::Orbit
|
||||
|| CameraMode == EPS_Editor_CameraMode::GizmoDrag)
|
||||
else if (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);
|
||||
}
|
||||
@@ -516,20 +568,52 @@ void APS_Editor_Pawn::HandleScaleMode(const FInputActionValue& Value)
|
||||
void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
APS_Editor_PlayerController* EditorPC = Cast<APS_Editor_PlayerController>(GetController());
|
||||
if (!EditorPC) return;
|
||||
UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager();
|
||||
if (!SM || !SM->IsAnythingSelected()) return;
|
||||
|
||||
// Soft-delete: hide actors instead of destroying (enables undo)
|
||||
TSharedPtr<FPS_Editor_DeleteAction> Action = MakeShared<FPS_Editor_DeleteAction>();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SM->GetSelectedActors())
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
Action->Actors.Add(Actor);
|
||||
}
|
||||
}
|
||||
|
||||
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_SelectionManager* SM = EditorPC->GetSelectionManager())
|
||||
if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager())
|
||||
{
|
||||
TArray<TWeakObjectPtr<AActor>> ToDelete = SM->GetSelectedActors();
|
||||
SM->ClearSelection();
|
||||
for (const TWeakObjectPtr<AActor>& Weak : ToDelete)
|
||||
{
|
||||
if (AActor* Actor = Weak.Get())
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Deleted %s"), *Actor->GetName());
|
||||
Actor->Destroy();
|
||||
}
|
||||
}
|
||||
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()
|
||||
TObjectPtr<UInputAction> IA_SnapToGround;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Undo;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UInputAction> IA_Redo;
|
||||
|
||||
// ---- Modifier key state ----
|
||||
bool bAltHeld = false;
|
||||
bool bCtrlHeld = false;
|
||||
@@ -191,6 +197,8 @@ private:
|
||||
void HandleScaleMode(const FInputActionValue& Value);
|
||||
void HandleDelete(const FInputActionValue& Value);
|
||||
void HandleSnapToGround(const FInputActionValue& Value);
|
||||
void HandleUndo(const FInputActionValue& Value);
|
||||
void HandleRedo(const FInputActionValue& Value);
|
||||
|
||||
// ---- Helpers ----
|
||||
void CreateInputActions();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_Pawn.h"
|
||||
|
||||
APS_Editor_PlayerController::APS_Editor_PlayerController()
|
||||
@@ -17,6 +19,20 @@ void APS_Editor_PlayerController::BeginPlay()
|
||||
// Create selection manager
|
||||
SelectionManager = NewObject<UPS_Editor_SelectionManager>(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)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "PS_Editor_PlayerController.generated.h"
|
||||
|
||||
class UPS_Editor_SelectionManager;
|
||||
class UPS_Editor_UndoManager;
|
||||
|
||||
/**
|
||||
* Player controller for PS_Editor runtime editing.
|
||||
@@ -21,6 +22,9 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_SelectionManager* GetSelectionManager() const { return SelectionManager; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "PS Editor")
|
||||
UPS_Editor_UndoManager* GetUndoManager() const { return UndoManager; }
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
@@ -29,5 +33,8 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_SelectionManager> SelectionManager;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UPS_Editor_UndoManager> UndoManager;
|
||||
|
||||
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
@@ -223,17 +225,22 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
||||
APlayerController* PC = OwnerPC.Get();
|
||||
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)
|
||||
{
|
||||
AActor* Actor = Weak.Get();
|
||||
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;
|
||||
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 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))
|
||||
{
|
||||
// Place actor so its bottom sits on the ground
|
||||
FVector NewLocation = Hit.ImpactPoint;
|
||||
NewLocation.Z += BottomOffset;
|
||||
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_MainWidget.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
|
||||
void APS_Editor_HUD::BeginPlay()
|
||||
@@ -11,6 +13,12 @@ void APS_Editor_HUD::BeginPlay()
|
||||
MainWidget = CreateWidget<UPS_Editor_MainWidget>(PC, UPS_Editor_MainWidget::StaticClass());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,75 @@
|
||||
#include "PS_Editor_MainWidget.h"
|
||||
#include "Components/TextBlock.h"
|
||||
#include "Components/VerticalBox.h"
|
||||
#include "Components/VerticalBoxSlot.h"
|
||||
#include "Components/Border.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.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/Input/SEditableTextBox.h"
|
||||
#include "Widgets/SOverlay.h"
|
||||
|
||||
void UPS_Editor_MainWidget::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager)
|
||||
{
|
||||
SelectionManager = InSelectionManager;
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
{
|
||||
// Build the widget tree using Slate (C++ driven UMG)
|
||||
return SNew(SOverlay)
|
||||
// Top bar
|
||||
// Top-left: Title + Toolbar
|
||||
+ SOverlay::Slot()
|
||||
.HAlign(HAlign_Left)
|
||||
.VAlign(VAlign_Top)
|
||||
.Padding(16.0f)
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(12.0f, 8.0f))
|
||||
SNew(SVerticalBox)
|
||||
// Title bar
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
SNew(SBorder)
|
||||
.BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.85f))
|
||||
.Padding(FMargin(12.0f, 8.0f))
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("PS Editor")))
|
||||
.ColorAndOpacity(FLinearColor(0.2f, 0.7f, 1.0f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(12.0f, 0.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("Edit Mode")))
|
||||
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 11))
|
||||
SNew(SHorizontalBox)
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(FText::FromString(TEXT("PS Editor")))
|
||||
.ColorAndOpacity(FLinearColor(0.2f, 0.7f, 1.0f, 1.0f))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 14))
|
||||
]
|
||||
+ SHorizontalBox::Slot()
|
||||
.AutoWidth()
|
||||
.VAlign(VAlign_Center)
|
||||
.Padding(12.0f, 0.0f, 0.0f, 0.0f)
|
||||
[
|
||||
SAssignNew(ModeText, STextBlock)
|
||||
.Text(FText::FromString(TEXT("Translate (E)")))
|
||||
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
|
||||
.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
|
||||
+ SOverlay::Slot()
|
||||
@@ -55,14 +82,306 @@ TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
|
||||
.Padding(FMargin(16.0f, 6.0f))
|
||||
[
|
||||
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))
|
||||
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
|
||||
.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))
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
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) ]
|
||||
];
|
||||
}
|
||||
|
||||
void UPS_Editor_MainWidget::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 "Blueprint/UserWidget.h"
|
||||
#include "PS_Editor_SelectionTypes.h"
|
||||
#include "PS_Editor_MainWidget.generated.h"
|
||||
|
||||
class UTextBlock;
|
||||
class UVerticalBox;
|
||||
class UBorder;
|
||||
class UPS_Editor_SelectionManager;
|
||||
|
||||
/**
|
||||
* Main editor widget for PS_Editor.
|
||||
* Serves as the root container for all editor UI panels.
|
||||
* Built entirely in C++ (no UMG blueprint).
|
||||
* Contains: title bar, transform mode toolbar, properties panel, help bar.
|
||||
* Built entirely in C++ using Slate.
|
||||
*/
|
||||
UCLASS()
|
||||
class PS_EDITOR_API UPS_Editor_MainWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Set the selection manager reference. Must be called after construction. */
|
||||
void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager);
|
||||
|
||||
protected:
|
||||
virtual TSharedRef<SWidget> RebuildWidget() 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