diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index f45f923..f80c25c 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -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(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(this, TEXT("IA_Undo")); + IA_Undo->ValueType = EInputActionValueType::Boolean; + EditorMappingContext->MapKey(IA_Undo, EKeys::Z); + + IA_Redo = NewObject(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(GetController())) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager()) + { + TSharedPtr Action = MakeShared(); + const TArray>& 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(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 Action = MakeShared(); + for (const TWeakObjectPtr& 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(GetController())) { - if (UPS_Editor_SelectionManager* SM = EditorPC->GetSelectionManager()) + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) { - TArray> ToDelete = SM->GetSelectedActors(); - SM->ClearSelection(); - for (const TWeakObjectPtr& 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(GetController())) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + UM->Redo(); } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h index c011272..4ca2b3c 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h @@ -161,6 +161,12 @@ private: UPROPERTY() TObjectPtr IA_SnapToGround; + UPROPERTY() + TObjectPtr IA_Undo; + + UPROPERTY() + TObjectPtr 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(); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 1c51bfa..3beed29 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -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(this); SelectionManager->Initialize(this); + + UndoManager = NewObject(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) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h index a8a7e36..4c45df5 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.h @@ -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 SelectionManager; + UPROPERTY() + TObjectPtr UndoManager; + void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp index d59cf18..ea9c473 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -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 UndoAction = MakeShared(); + UndoAction->Description = FString::Printf(TEXT("Snap to ground %d actor(s)"), SelectedActors.Num()); + for (const TWeakObjectPtr& 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(PC)) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + UM->RecordAction(UndoAction); + } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp new file mode 100644 index 0000000..87de4a6 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.cpp @@ -0,0 +1,105 @@ +#include "PS_Editor_UndoManager.h" + +void UPS_Editor_UndoManager::RecordAction(TSharedPtr 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& 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& 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 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(Action.Get()); + for (const TWeakObjectPtr& Weak : DeleteAction->Actors) + { + if (AActor* Actor = Weak.Get()) + { + if (Actor->IsHidden()) + { + Actor->Destroy(); + } + } + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h new file mode 100644 index 0000000..e6c42be --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_UndoManager.h @@ -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> Actors; + TArray OldTransforms; + TArray 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> Actors; + + virtual void Execute() override + { + for (const TWeakObjectPtr& Weak : Actors) + { + if (AActor* Actor = Weak.Get()) + { + Actor->SetActorHiddenInGame(true); + Actor->SetActorEnableCollision(false); + Actor->SetActorTickEnabled(false); + } + } + } + + virtual void Undo() override + { + for (const TWeakObjectPtr& 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 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> 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 Action); +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp index 240b8f9..14d19d5 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp @@ -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(PC, UPS_Editor_MainWidget::StaticClass()); if (MainWidget) { + // Wire the SelectionManager to the UI + if (APS_Editor_PlayerController* EditorPC = Cast(PC)) + { + MainWidget->SetSelectionManager(EditorPC->GetSelectionManager()); + } + MainWidget->AddToViewport(0); } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index aefb361..a003408 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -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 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 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 UPS_Editor_MainWidget::BuildToolbar() +{ + auto MakeButton = [this](const FText& Label, EPS_Editor_TransformMode Mode) -> TSharedRef + { + 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 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& X, TSharedPtr& Y, TSharedPtr& Z, const FLinearColor& ColorX, const FLinearColor& ColorY, const FLinearColor& ColorZ) -> TSharedRef + { + auto MakeField = [&](TSharedPtr& Field, const FLinearColor& Col) -> TSharedRef + { + 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& 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>& 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& 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& 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(PC)) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + 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); + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 46e3382..d9a4f73 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -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 RebuildWidget() override; - virtual void NativeConstruct() override; + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + +private: + UPROPERTY() + TWeakObjectPtr SelectionManager; + + // ---- Slate widget refs for dynamic updates ---- + TSharedPtr ModeText; + TSharedPtr SelectionInfoText; + + // Transform fields + TSharedPtr LocX, LocY, LocZ; + TSharedPtr RotX, RotY, RotZ; + TSharedPtr ScaX, ScaY, ScaZ; + + // ---- Helpers ---- + TSharedRef BuildToolbar(); + TSharedRef 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; };