Files
UnrealEngine/Engine/Source/Editor/Persona/Private/SPoseEditor.cpp
2025-05-18 13:04:45 +08:00

1032 lines
29 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SPoseEditor.h"
#include "AnimPreviewInstance.h"
#include "Misc/MessageDialog.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Widgets/Input/SSpinBox.h"
#include "Widgets/Layout/SSplitter.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "ScopedTransaction.h"
#include "Widgets/Input/SSearchBox.h"
#include "Animation/AnimSingleNodeInstance.h"
#include "UObject/UObjectIterator.h"
#include "HAL/PlatformApplicationMisc.h"
#include "Widgets/Text/SInlineEditableTextBlock.h"
#include "Framework/Commands/GenericCommands.h"
#include "PoseEditorCommands.h"
#define LOCTEXT_NAMESPACE "AnimPoseEditor"
static const FName ColumnId_PoseNameLabel("Pose Name");
static const FName ColumnID_PoseWeightLabel("Weight");
static const FName ColumnId_CurveNameLabel("Curve Name");
static const FName ColumnID_CurveValueLabel("Curve Value");
const float MaxPoseWeight = 1.f;
//////////////////////////////////////////////////////////////////////////
// SPoseEditor
void SPoseEditor::Construct(const FArguments& InArgs, const TSharedRef<class IPersonaToolkit>& InPersonaToolkit, const TSharedRef<IEditableSkeleton>& InEditableSkeleton, const TSharedRef<class IPersonaPreviewScene>& InPreviewScene)
{
PoseAssetObj = InArgs._PoseAsset;
check(PoseAssetObj);
SAnimEditorBase::Construct(SAnimEditorBase::FArguments()
.DisplayAnimTimeline(false),
InPreviewScene);
NonScrollEditorPanels->AddSlot()
.FillHeight(1)
.Padding(0, 10)
[
SNew( SPoseViewer, InPersonaToolkit, InEditableSkeleton, InPreviewScene)
.PoseAsset(PoseAssetObj)
];
}
//////////////////////////////////////////////////////////////////////////
// SPoseListRow
FText SPoseListRow::GetName() const
{
return (FText::FromName(Item->Name));
}
void SPoseListRow::OnNameCommitted(const FText& InText, ETextCommit::Type InCommitType) const
{
// for now only allow enter
// because it is important to keep the unique names per pose
if (InCommitType == ETextCommit::OnEnter)
{
FName NewName = FName(*InText.ToString());
FName OldName = Item->Name;
if (PoseViewerPtr.IsValid() && PoseViewerPtr.Pin()->ModifyName(OldName, NewName))
{
Item->Name = NewName;
}
}
}
bool SPoseListRow::OnVerifyNameChanged(const FText& InText, FText& OutErrorMessage)
{
bool bVerifyName = false;
FName NewName = FName(*InText.ToString());
if (NewName == NAME_None)
{
OutErrorMessage = LOCTEXT("EmptyPoseName", "Poses must have a name!");
}
if (PoseViewerPtr.IsValid())
{
UPoseAsset* PoseAsset = PoseViewerPtr.Pin()->PoseAssetPtr.Get();
if (PoseAsset != nullptr)
{
if (PoseAsset->ContainsPose(NewName))
{
OutErrorMessage = LOCTEXT("NameAlreadyUsedByTheSameAsset", "The name is used by another pose within the same asset. Please choose another name.");
}
else
{
bVerifyName = true;
}
}
}
return bVerifyName;
}
void SPoseListRow::Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView, const TSharedRef<IPersonaPreviewScene>& InPreviewScene)
{
Item = InArgs._Item;
PoseViewerPtr = InArgs._PoseViewer;
FilterText = InArgs._FilterText;
PreviewScenePtr = InPreviewScene;
check(Item.IsValid());
SMultiColumnTableRow< TSharedPtr<FDisplayedPoseInfo> >::Construct(FSuperRowType::FArguments(), InOwnerTableView);
}
TSharedRef< SWidget > SPoseListRow::GenerateWidgetForColumn(const FName& ColumnName)
{
if (ColumnName == ColumnId_PoseNameLabel)
{
TSharedPtr< SInlineEditableTextBlock > InlineWidget;
TSharedRef<SWidget> NameWidget =
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(5.0f)
.VAlign(VAlign_Center)
[
SAssignNew(InlineWidget, SInlineEditableTextBlock)
.Text(this, &SPoseListRow::GetName)
.HighlightText(FilterText)
.ToolTipText(LOCTEXT("PoseName_ToolTip", "Modify Pose Name - Make sure this name is unique among all curves per skeleton."))
.MaximumLength(NAME_SIZE-1)
.OnVerifyTextChanged(this, &SPoseListRow::OnVerifyNameChanged)
.OnTextCommitted(this, &SPoseListRow::OnNameCommitted)
];
Item->OnRenameRequested.BindSP(InlineWidget.Get(), &SInlineEditableTextBlock::EnterEditingMode);
return NameWidget;
}
else
{
// Encase the SSpinbox in an SVertical box so we can apply padding. Setting ItemHeight on the containing SListView has no effect :-(
return
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(5.0f)
.VAlign(VAlign_Center)
[
SNew(SSpinBox<float>)
.MinSliderValue(-1.f)
.MaxSliderValue(1.f)
.MinValue(-MaxPoseWeight)
.MaxValue(MaxPoseWeight)
.Value(this, &SPoseListRow::GetWeight)
.OnValueChanged(this, &SPoseListRow::OnPoseWeightChanged)
.OnValueCommitted(this, &SPoseListRow::OnPoseWeightValueCommitted)
.IsEnabled(this, &SPoseListRow::CanChangeWeight)
];
}
}
void SPoseListRow::OnPoseWeightChanged(float NewWeight)
{
Item->Weight = NewWeight;
if (PoseViewerPtr.IsValid())
{
TSharedPtr<SPoseViewer> PoseViewer = PoseViewerPtr.Pin();
PoseViewer->AddCurveOverride(Item->Name, Item->Weight);
PreviewScenePtr.Pin()->InvalidateViews();
}
}
void SPoseListRow::OnPoseWeightValueCommitted(float NewWeight, ETextCommit::Type CommitType)
{
if (CommitType == ETextCommit::OnEnter || CommitType == ETextCommit::OnUserMovedFocus)
{
float NewValidWeight = FMath::Clamp(NewWeight, -MaxPoseWeight, MaxPoseWeight);
OnPoseWeightChanged(NewValidWeight);
}
}
float SPoseListRow::GetWeight() const
{
return Item->Weight;
}
bool SPoseListRow::CanChangeWeight() const
{
if (PoseViewerPtr.IsValid())
{
return PoseViewerPtr.Pin()->IsBasePose(Item->Name) == false;
}
else
{
return false;
}
}
//////////////////////////////////////////////////////////////////////////
// SCurveListRow
FText SCurveListRow::GetName() const
{
return (FText::FromName(Item->Name));
}
FText SCurveListRow::GetValue() const
{
FText ValueText;
if (PoseViewerPtr.IsValid())
{
TSharedPtr<SPoseViewer> PoseViewer = PoseViewerPtr.Pin();
// Get pose asset
UPoseAsset* PoseAsset = PoseViewer->PoseAssetPtr.Get();
if (PoseAsset != nullptr)
{
// Get selected row (only show values if only one selected)
TArray< TSharedPtr<FDisplayedPoseInfo> > SelectedRows = PoseViewer->PoseListView->GetSelectedItems();
if (SelectedRows.Num() == 1)
{
TSharedPtr<FDisplayedPoseInfo> PoseInfo = SelectedRows[0];
// Get pose index that we have selected
int32 PoseIndex = PoseAsset->GetPoseIndexByName(PoseInfo->Name);
int32 CurveIndex = PoseAsset->GetCurveIndexByName(Item->Name);
float CurveValue = 0.f;
bool bSuccess = PoseAsset->GetCurveValue(PoseIndex, CurveIndex, CurveValue);
if (bSuccess)
{
ValueText = FText::FromString(FString::Printf(TEXT("%f"), CurveValue));
}
}
}
}
return ValueText;
}
void SCurveListRow::Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView)
{
Item = InArgs._Item;
PoseViewerPtr = InArgs._PoseViewer;
check(Item.IsValid());
SMultiColumnTableRow< TSharedPtr<FDisplayedCurveInfo> >::Construct(FSuperRowType::FArguments(), InOwnerTableView);
}
TSharedRef< SWidget > SCurveListRow::GenerateWidgetForColumn(const FName& ColumnName)
{
// for now we have one colume
if (ColumnName == ColumnId_CurveNameLabel)
{
return
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(5.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(this, &SCurveListRow::GetName)
];
}
else
{
return
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(5.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(this, &SCurveListRow::GetValue)
];
}
}
//////////////////////////////////////////////////////////////////////////
// SPoseViewer
void SPoseViewer::Construct(const FArguments& InArgs, const TSharedRef<IPersonaToolkit>& InPersonaToolkit, const TSharedRef<IEditableSkeleton>& InEditableSkeleton, const TSharedRef<IPersonaPreviewScene>& InPreviewScene)
{
PreviewScenePtr = InPreviewScene;
PersonaToolkitPtr = InPersonaToolkit;
EditableSkeletonPtr = InEditableSkeleton;
PoseAssetPtr = InArgs._PoseAsset;
NewPoseName = UPoseAsset::GetUniquePoseName(PoseAssetPtr.Get());
InPreviewScene->RegisterOnPreviewMeshChanged(FOnPreviewMeshChanged::CreateSP(this, &SPoseViewer::OnPreviewMeshChanged));
OnDelegatePoseListChangedDelegateHandle = PoseAssetPtr->RegisterOnPoseListChanged(UPoseAsset::FOnPoseListChanged::CreateSP(this, &SPoseViewer::OnPoseAssetModified));
// Register and bind all our menu commands
FPoseEditorCommands::Register();
BindCommands();
ChildSlot
[
SNew(SSplitter)
.Orientation(EOrientation::Orient_Horizontal)
// Pose List
+SSplitter::Slot()
.Value(1)
[
SNew(SBox)
.Padding(5)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0, 2)
[
SNew(SHorizontalBox)
// Filter entry
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SAssignNew(NameFilterBox, SSearchBox)
.SelectAllTextWhenFocused(true)
.OnTextChanged(this, &SPoseViewer::OnFilterTextChanged)
.OnTextCommitted(this, &SPoseViewer::OnFilterTextCommitted)
]
]
+ SVerticalBox::Slot()
.FillHeight(1)
.Padding(0, 2)
[
SAssignNew(PoseListView, SPoseListType)
.ListItemsSource(&PoseList)
.OnGenerateRow(this, &SPoseViewer::GeneratePoseRow)
.OnContextMenuOpening(this, &SPoseViewer::OnGetContextMenuContent)
.OnMouseButtonDoubleClick(this, &SPoseViewer::OnListDoubleClick)
.HeaderRow
(
SNew(SHeaderRow)
+ SHeaderRow::Column(ColumnId_PoseNameLabel)
.DefaultLabel(LOCTEXT("PoseNameLabel", "Pose Name"))
+ SHeaderRow::Column(ColumnID_PoseWeightLabel)
.DefaultLabel(LOCTEXT("PoseWeightLabel", "Weight"))
)
]
]
]
// Curve List
+SSplitter::Slot()
.Value(1)
[
SNew(SBorder)
.Padding(8)
.BorderImage(FAppStyle::GetBrush("ToolPanel.DarkGroupBorder"))
[
SAssignNew(CurveListView, SCurveListType)
.ListItemsSource(&CurveList)
.OnGenerateRow(this, &SPoseViewer::GenerateCurveRow)
.OnContextMenuOpening(this, &SPoseViewer::OnGetContextMenuContentForCurveList)
.HeaderRow
(
SNew(SHeaderRow)
+ SHeaderRow::Column(ColumnId_CurveNameLabel)
.DefaultLabel(LOCTEXT("CurveNameLabel", "Curve Name"))
+ SHeaderRow::Column(ColumnID_CurveValueLabel)
.DefaultLabel(LOCTEXT("CurveValueLabel", "Value"))
)
]
]
];
CreatePoseList();
CreateCurveList();
}
void SPoseViewer::OnPreviewMeshChanged(class USkeletalMesh* OldPreviewMesh, class USkeletalMesh* NewPreviewMesh)
{
CreatePoseList(NameFilterBox->GetText().ToString());
CreateCurveList(NameFilterBox->GetText().ToString());
}
void SPoseViewer::OnFilterTextChanged(const FText& SearchText)
{
FilterText = SearchText;
CreatePoseList(SearchText.ToString());
CreateCurveList(SearchText.ToString());
}
void SPoseViewer::OnFilterTextCommitted(const FText& SearchText, ETextCommit::Type CommitInfo)
{
// Just do the same as if the user typed in the box
OnFilterTextChanged(SearchText);
}
TSharedRef<ITableRow> SPoseViewer::GeneratePoseRow(TSharedPtr<FDisplayedPoseInfo> InInfo, const TSharedRef<STableViewBase>& OwnerTable)
{
check(InInfo.IsValid());
return
SNew(SPoseListRow, OwnerTable, PreviewScenePtr.Pin().ToSharedRef())
.Item(InInfo)
.PoseViewer(SharedThis(this))
.FilterText(GetFilterText());
}
TSharedRef<ITableRow> SPoseViewer::GenerateCurveRow(TSharedPtr<FDisplayedCurveInfo> InInfo, const TSharedRef<STableViewBase>& OwnerTable)
{
check(InInfo.IsValid());
return
SNew(SCurveListRow, OwnerTable)
.Item(InInfo)
.PoseViewer(SharedThis(this));
}
bool SPoseViewer::IsPoseSelected() const
{
// @todo: make sure not to delete base Curve
TArray< TSharedPtr< FDisplayedPoseInfo > > SelectedRows = PoseListView->GetSelectedItems();
return SelectedRows.Num() > 0;
}
bool SPoseViewer::IsSinglePoseSelected() const
{
// @todo: make sure not to delete base Curve
TArray< TSharedPtr< FDisplayedPoseInfo > > SelectedRows = PoseListView->GetSelectedItems();
return SelectedRows.Num() == 1;
}
bool SPoseViewer::IsCurveSelected() const
{
// @todo: make sure not to delete base pose
TArray< TSharedPtr< FDisplayedCurveInfo > > SelectedRows = CurveListView->GetSelectedItems();
return SelectedRows.Num() > 0;
}
// Restart Animation state for all instnace that belong to the current Skeleton
void RestartAnimations(const USkeleton* CurrentSkeleton)
{
for (FThreadSafeObjectIterator Iter(USkeletalMeshComponent::StaticClass()); Iter; ++Iter)
{
USkeletalMeshComponent* SkeletalMeshComponent = Cast<USkeletalMeshComponent>(*Iter);
if (SkeletalMeshComponent->GetSkeletalMeshAsset() && SkeletalMeshComponent->GetSkeletalMeshAsset()->GetSkeleton() == CurrentSkeleton)
{
SkeletalMeshComponent->InitAnim(true);
}
}
}
void SPoseViewer::OnDeletePoses()
{
TArray< TSharedPtr< FDisplayedPoseInfo > > SelectedRows = PoseListView->GetSelectedItems();
FScopedTransaction Transaction(LOCTEXT("DeletePoses", "Delete Poses"));
PoseAssetPtr.Get()->Modify();
TArray<FName> PosesToDelete;
for (int RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex)
{
PosesToDelete.Add(SelectedRows[RowIndex]->Name);
}
PoseAssetPtr.Get()->DeletePoses(PosesToDelete);
// reinit animation
RestartAnimations(&(EditableSkeletonPtr.Pin()->GetSkeleton()));
RestartPreviewComponent();
CreatePoseList(NameFilterBox->GetText().ToString());
}
void SPoseViewer::OnRenamePose()
{
TArray< TSharedPtr< FDisplayedPoseInfo > > SelectedRows = PoseListView->GetSelectedItems();
if (SelectedRows.Num() > 0)
{
TSharedPtr<FDisplayedPoseInfo> SelectedRow = SelectedRows[0];
if (SelectedRow.IsValid())
{
SelectedRow->OnRenameRequested.ExecuteIfBound();
}
}
}
void SPoseViewer::OnDeleteCurves()
{
TArray< TSharedPtr< FDisplayedCurveInfo > > SelectedRows = CurveListView->GetSelectedItems();
FScopedTransaction Transaction(LOCTEXT("DeleteCurves", "Delete Curves"));
PoseAssetPtr.Get()->Modify();
TArray<FName> CurvesToDelete;
for (int RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex)
{
CurvesToDelete.Add(SelectedRows[RowIndex]->Name);
}
PoseAssetPtr.Get()->DeleteCurves(CurvesToDelete);
CreateCurveList(NameFilterBox->GetText().ToString());
}
void SPoseViewer::OnPastePoseNamesFromClipBoard(bool bSelectedOnly)
{
FString PastedString;
FPlatformApplicationMisc::ClipboardPaste(PastedString);
if (PastedString.IsEmpty() == false)
{
TArray<FString> ListOfNames;
PastedString.ParseIntoArrayLines(ListOfNames);
if (ListOfNames.Num() > 0)
{
TArray<FName> PosesToRename;
if (bSelectedOnly)
{
TArray< TSharedPtr< FDisplayedPoseInfo > > SelectedRows = PoseListView->GetSelectedItems();
for (int RowIndex = 0; RowIndex < SelectedRows.Num(); ++RowIndex)
{
PosesToRename.Add(SelectedRows[RowIndex]->Name);
}
}
else
{
for (auto& PoseItem : PoseList)
{
PosesToRename.Add(PoseItem->Name);
}
}
if (PosesToRename.Num() > 0)
{
FScopedTransaction Transaction(LOCTEXT("PasteNames", "Paste Curve Names"));
PoseAssetPtr.Get()->Modify();
int32 TotalItemCount = FMath::Min(PosesToRename.Num(), ListOfNames.Num());
for (int32 PoseIndex = 0; PoseIndex < TotalItemCount; ++PoseIndex)
{
ModifyName(PosesToRename[PoseIndex], FName(*ListOfNames[PoseIndex]), true);
}
CreatePoseList(NameFilterBox->GetText().ToString());
}
}
}
}
FReply SPoseViewer::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
if (UICommandList.IsValid() && UICommandList->ProcessCommandBindings(InKeyEvent))
{
return FReply::Handled();
}
return FReply::Unhandled();
}
void SPoseViewer::BindCommands()
{
// This should not be called twice on the same instance
check(!UICommandList.IsValid());
UICommandList = MakeShareable(new FUICommandList);
FUICommandList& CommandList = *UICommandList;
// Grab the list of menu commands to bind...
const FPoseEditorCommands& PoseEditorCommands = FPoseEditorCommands::Get();
// ...and bind them all
CommandList.MapAction(
FGenericCommands::Get().Rename,
FExecuteAction::CreateSP(this, &SPoseViewer::OnRenamePose),
FCanExecuteAction::CreateSP(this, &SPoseViewer::IsSinglePoseSelected));
CommandList.MapAction(
FGenericCommands::Get().Delete,
FExecuteAction::CreateSP(this, &SPoseViewer::OnDeletePoses),
FCanExecuteAction::CreateSP(this, &SPoseViewer::IsPoseSelected));
CommandList.MapAction(
FGenericCommands::Get().Paste,
FExecuteAction::CreateSP(this, &SPoseViewer::OnPastePoseNamesFromClipBoard, true),
FCanExecuteAction::CreateSP(this, &SPoseViewer::IsPoseSelected));
CommandList.MapAction(
PoseEditorCommands.PasteAllNames,
FExecuteAction::CreateSP(this, &SPoseViewer::OnPastePoseNamesFromClipBoard, false),
FCanExecuteAction());
CommandList.MapAction(
PoseEditorCommands.UpdatePoseToCurrent,
FExecuteAction::CreateSP(this, &SPoseViewer::UpdateSelectedPoseWithCurrent),
FCanExecuteAction(),
FGetActionCheckState(),
FIsActionButtonVisible::CreateLambda([this]()
{
const TArray<TSharedPtr<FDisplayedPoseInfo>> SelectedRows = PoseListView->GetSelectedItems();
return SelectedRows.Num() == 1;
}));
CommandList.MapAction(
PoseEditorCommands.AddPoseFromCurrent,
FExecuteAction::CreateSP(this, &SPoseViewer::AddPoseWithCurrent),
FCanExecuteAction::CreateLambda([this]() -> bool
{
if (const UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
FText Temp;
return PoseAsset->SourceAnimation == nullptr && IsNewPoseNameValid(Temp);
}
return false;
}),
FGetActionCheckState(),
FIsActionButtonVisible::CreateLambda([this]() -> bool
{
if (const UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
return PoseAsset->SourceAnimation == nullptr;
}
return false;
}));
CommandList.MapAction(
PoseEditorCommands.AddPoseFromReference,
FExecuteAction::CreateSP(this, &SPoseViewer::AddPoseWithReference),
FCanExecuteAction::CreateLambda([this]() -> bool
{
if (const UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
FText Temp;
return PoseAsset->SourceAnimation == nullptr && IsNewPoseNameValid(Temp);
}
return false;
}),
FGetActionCheckState(),
FIsActionButtonVisible::CreateLambda([this]() -> bool
{
if (const UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
return PoseAsset->SourceAnimation == nullptr;
}
return false;
}));
}
TSharedPtr<SWidget> SPoseViewer::OnGetContextMenuContent()
{
const bool bShouldCloseWindowAfterMenuSelection = true;
FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, UICommandList);
const FPoseEditorCommands& PoseEditorCommands = FPoseEditorCommands::Get();
MenuBuilder.AddMenuEntry(PoseEditorCommands.PasteAllNames);
MenuBuilder.BeginSection("PoseAction", LOCTEXT("SelectedItems", "Selected Item Actions"));
{
MenuBuilder.AddMenuEntry(PoseEditorCommands.UpdatePoseToCurrent);
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete, NAME_None, LOCTEXT("DeletePoseButtonLabel", "Delete"), LOCTEXT("DeletePoseButtonTooltip", "Delete the selected pose(s)"));
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Rename, NAME_None, LOCTEXT("RenamePoseButtonLabel", "Rename"), LOCTEXT("RenamePoseButtonTooltip", "Renames the selected pose"));
MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste, NAME_None, LOCTEXT("PastePoseNamesButtonLabel", "Paste Selected"), LOCTEXT("PastePoseNamesButtonTooltip", "Paste the selected pose names from clipboard"));
}
MenuBuilder.EndSection();
if (const UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
if (PoseAsset->SourceAnimation == nullptr)
{
MenuBuilder.BeginSection("AddPoseAction", LOCTEXT("PosesSection", "Poses"));
{
MenuBuilder.AddSubMenu(LOCTEXT("AddPoseSubMenu", "Add Pose"), LOCTEXT("PosesSubMenuToolTip", "Adding new Pose Related Actions"), FNewMenuDelegate::CreateLambda([this]( FMenuBuilder& SubMenuBuilder)
{
SubMenuBuilder.AddMenuEntry(FPoseEditorCommands::Get().AddPoseFromCurrent, NAME_None, TAttribute<FText>(), TAttribute<FText>(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Persona.AssetClass.Animation"));
SubMenuBuilder.AddMenuEntry(FPoseEditorCommands::Get().AddPoseFromReference, NAME_None, TAttribute<FText>(), TAttribute<FText>(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Persona.AssetClass.Skeleton"));
SubMenuBuilder.AddVerifiedEditableText(LOCTEXT("NewPoseLabel", "Pose Name"), LOCTEXT("NewPoseTooltip", "Tooltip"), FSlateIcon(),
TAttribute<FText>::CreateLambda([this]() { return FText::FromName(NewPoseName); }),
FOnVerifyTextChanged::CreateLambda([this](const FText& NewText, FText& OutMessage)-> bool
{
NewPoseName = FName(*NewText.ToString());
return IsNewPoseNameValid(OutMessage);
}));
}));
}
MenuBuilder.EndSection();
}
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
bool SPoseViewer::IsNewPoseNameValid(FText& OutReason) const
{
if (const UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
if (PoseAsset->ContainsPose(NewPoseName))
{
OutReason = LOCTEXT("NewPoseAlreadyExistsMessage", "Pose with this name already exists");
return false;
}
}
if(!NewPoseName.IsValidObjectName(OutReason))
{
return false;
}
if (NewPoseName == NAME_None)
{
OutReason = LOCTEXT("NameNonePoseName", "Pose name cannot be empty or None");
return false;
}
return true;
}
TSharedPtr<SWidget> SPoseViewer::OnGetContextMenuContentForCurveList() const
{
const bool bShouldCloseWindowAfterMenuSelection = true;
FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, NULL);
MenuBuilder.BeginSection("CurveAction", LOCTEXT("CurveActions", "Selected Item Actions"));
{
FUIAction Action = FUIAction(FExecuteAction::CreateSP(const_cast<SPoseViewer*>(this), &SPoseViewer::OnDeleteCurves),
FCanExecuteAction::CreateSP(this, &SPoseViewer::IsCurveSelected));
const FText MenuLabel = LOCTEXT("DeleteCurveButtonLabel", "Delete");
const FText MenuToolTip = LOCTEXT("DeleteCurveButtonTooltip", "Deletes the selected animation curve.");
MenuBuilder.AddMenuEntry(MenuLabel, MenuToolTip, FSlateIcon(), Action);
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
void SPoseViewer::OnListDoubleClick(TSharedPtr<FDisplayedPoseInfo> InItem)
{
if(InItem.IsValid())
{
const float CurrentWeight = InItem->Weight;
// Clear all preview poses
for (TSharedPtr<FDisplayedPoseInfo>& Pose : PoseList)
{
Pose->Weight = 0.f;
AddCurveOverride(Pose->Name, 0.f);
}
// If current weight was already at 1.0, do nothing (we are setting it to zero)
if (!FMath::IsNearlyEqual(CurrentWeight, 1.f))
{
// Otherwise set to 1.0
InItem->Weight = 1.f;
AddCurveOverride(InItem->Name, 1.f);
}
// Force update viewport
PreviewScenePtr.Pin()->InvalidateViews();
}
}
void SPoseViewer::CreatePoseList(const FString& SearchText)
{
PoseList.Empty();
if (PoseAssetPtr.IsValid())
{
UPoseAsset* PoseAsset = PoseAssetPtr.Get();
TArray<FName> PoseNames = PoseAsset->GetPoseFNames();
if (PoseNames.Num() > 0)
{
bool bDoFiltering = !SearchText.IsEmpty();
for (const FName& PoseName : PoseNames)
{
if (bDoFiltering && !PoseName.ToString().Contains(SearchText))
{
continue; // Skip items that don't match our filter
}
const TSharedRef<FDisplayedPoseInfo> Info = FDisplayedPoseInfo::Make(PoseName);
float* Weight = OverrideCurves.Find(PoseName);
if (Weight)
{
Info->Weight = *Weight;
}
PoseList.Add(Info);
}
}
}
PoseListView->RequestListRefresh();
}
void SPoseViewer::CreateCurveList(const FString& SearchText)
{
CurveList.Empty();
if (PoseAssetPtr.IsValid())
{
UPoseAsset* PoseAsset = PoseAssetPtr.Get();
for (const FName& CurveName : PoseAsset->GetCurveFNames())
{
const TSharedRef<FDisplayedCurveInfo> Info = FDisplayedCurveInfo::Make(CurveName);
CurveList.Add(Info);
}
}
CurveListView->RequestListRefresh();
}
void SPoseViewer::AddCurveOverride(const FName& Name, float Weight)
{
float& Value = OverrideCurves.FindOrAdd(Name);
Value = Weight;
UAnimSingleNodeInstance* SingleNodeInstance = Cast<UAnimSingleNodeInstance>(GetAnimInstance());
if (SingleNodeInstance)
{
SingleNodeInstance->SetPreviewCurveOverride(Name, Value, false);
}
}
void SPoseViewer::RemoveCurveOverride(FName& Name)
{
OverrideCurves.Remove(Name);
UAnimSingleNodeInstance* SingleNodeInstance = Cast<UAnimSingleNodeInstance>(GetAnimInstance());
if (SingleNodeInstance)
{
SingleNodeInstance->SetPreviewCurveOverride(Name, 0.f, true);
}
}
SPoseViewer::~SPoseViewer()
{
if (PreviewScenePtr.IsValid())
{
PreviewScenePtr.Pin()->UnregisterOnPreviewMeshChanged(this);
PreviewScenePtr.Pin()->UnregisterOnAnimChanged(this);
}
if (PoseAssetPtr.IsValid())
{
PoseAssetPtr->UnregisterOnPoseListChanged(OnDelegatePoseListChangedDelegateHandle);
}
}
void SPoseViewer::RestartPreviewComponent()
{
// it needs reinitialization of animation system
// so that pose blender can reinitialize names and so on correctly
if (PreviewScenePtr.IsValid())
{
UDebugSkelMeshComponent* PreviewComponent = PreviewScenePtr.Pin()->GetPreviewMeshComponent();
if (PreviewComponent)
{
// we can't wait until next tick for this in this case
// since animation is initialized here
// so we calculate curve here
PreviewComponent->RecalcRequiredCurves();
PreviewComponent->InitAnim(true);
for (auto Iter = OverrideCurves.CreateConstIterator(); Iter; ++Iter)
{
// refresh curve names that are active
AddCurveOverride(Iter.Key(), Iter.Value());
}
}
}
}
void SPoseViewer::OnPoseAssetModified()
{
CreatePoseList(NameFilterBox->GetText().ToString());
CreateCurveList(NameFilterBox->GetText().ToString());
RestartPreviewComponent();
}
void SPoseViewer::ApplyCustomCurveOverride(UAnimInstance* AnimInstance) const
{
for (auto Iter = OverrideCurves.CreateConstIterator(); Iter; ++Iter)
{
// @todo we might want to save original curve flags? or just change curve to apply flags only
AnimInstance->AddCurveValue(Iter.Key(), Iter.Value());
}
}
UAnimInstance* SPoseViewer::GetAnimInstance() const
{
return PreviewScenePtr.Pin()->GetPreviewMeshComponent()->GetAnimInstance();
}
bool SPoseViewer::ModifyName(FName OldName, FName NewName, bool bSilence)
{
if(PoseAssetPtr.Get()->ContainsPose(NewName))
{
// warn users
// if so, verify if this name is still okay
if (!bSilence)
{
const EAppReturnType::Type Response = FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("UseSameNameConfirm", "The name already exists. Would you like to reuse the name? This can cause conflict of curve data."));
if (Response == EAppReturnType::No)
{
return false;
}
}
}
FScopedTransaction Transaction(LOCTEXT("RenamePoses", "Rename Pose"));
PoseAssetPtr.Get()->Modify();
// I think this might have to be delegate of the top window
if (PoseAssetPtr.Get()->ModifyPoseName(OldName, NewName) == false)
{
return false;
}
// now refresh pose data
float* Value = OverrideCurves.Find(OldName);
if (Value)
{
AddCurveOverride(NewName, *Value);
RemoveCurveOverride(OldName);
}
return true;
}
bool SPoseViewer::IsBasePose(FName PoseName) const
{
if (PoseAssetPtr.IsValid() && PoseAssetPtr->IsValidAdditive())
{
int32 PoseIndex = PoseAssetPtr->GetPoseIndexByName(PoseName);
return (PoseIndex == PoseAssetPtr->GetBasePoseIndex());
}
return false;
}
void SPoseViewer::UpdateSelectedPoseWithCurrent()
{
TArray<TSharedPtr<FDisplayedPoseInfo>> SelectedRows = PoseListView->GetSelectedItems();
UPoseAsset* PoseAsset = PoseAssetPtr.Get();
if (SelectedRows.Num() == 1 && PoseAsset && PreviewScenePtr.IsValid())
{
UDebugSkelMeshComponent* PreviewComponent = PreviewScenePtr.Pin()->GetPreviewMeshComponent();
const USkeleton* Skeleton = PoseAsset->GetSkeleton();
if (PreviewComponent && Skeleton)
{
const FName PoseName = SelectedRows[0]->Name;
FScopedTransaction Transaction(LOCTEXT("UpdatePose", "Update Pose from Viewport"));
PoseAsset->Modify();
PoseAsset->AddOrUpdatePose(PoseName, PreviewComponent, false);
// Reset bone modifiers in case the user has created a pose using them - resetting them to the 'base' pose
if(UAnimPreviewInstance* PreviewInstance = Cast<UAnimPreviewInstance>(PreviewComponent->GetAnimInstance()))
{
PreviewInstance->ResetModifiedBone();
}
// Reinitialize animation preview
RestartAnimations(Skeleton);
RestartPreviewComponent();
}
}
}
void SPoseViewer::AddPoseWithCurrent()
{
if(UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
UDebugSkelMeshComponent* PreviewComponent = PreviewScenePtr.Pin()->GetPreviewMeshComponent();
USkeleton* Skeleton = PoseAsset->GetSkeleton();
if (PreviewComponent && Skeleton)
{
PoseAsset->AddOrUpdatePose(NewPoseName, PreviewComponent);
NewPoseName = UPoseAsset::GetUniquePoseName(PoseAsset);
}
}
}
void SPoseViewer::AddPoseWithReference()
{
if(UPoseAsset* PoseAsset = PoseAssetPtr.Get())
{
const UDebugSkelMeshComponent* PreviewComponent = PreviewScenePtr.Pin()->GetPreviewMeshComponent();
USkeleton* Skeleton = PoseAsset->GetSkeleton();
if (PreviewComponent && Skeleton)
{
PoseAsset->AddReferencePose(NewPoseName, PreviewComponent->GetReferenceSkeleton());
NewPoseName = UPoseAsset::GetUniquePoseName(PoseAsset);
}
}
}
#undef LOCTEXT_NAMESPACE