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

446 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SControlRigMappingWindow.h"
#include "Misc/MessageDialog.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#include "Styling/AppStyle.h"
#include "Widgets/Layout/SSeparator.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/SToolTip.h"
#include "IDocumentation.h"
#include "ScopedTransaction.h"
#include "IContentBrowserSingleton.h"
#include "ContentBrowserModule.h"
#include "Engine/Blueprint.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/NodeMappingProviderInterface.h"
#include "AnimationRuntime.h"
#define LOCTEXT_NAMESPACE "SControlRigMappingWindow"
//////////////////////////////////////////////////////////////////////////
// SAnimationMappingWindow
void SControlRigMappingWindow::Construct(const FArguments& InArgs, const TWeakObjectPtr<class USkeletalMesh>& InEditableMesh, FSimpleMulticastDelegate& InOnPostUndo)
{
EditableSkeletalMeshPtr = InEditableMesh;
InOnPostUndo.Add(FSimpleDelegate::CreateSP(this, &SControlRigMappingWindow::PostUndo));
const FString DocLink = TEXT("Shared/Editors/Persona");
ChildSlot
[
SNew (SVerticalBox)
+ SVerticalBox::Slot()
.Padding(5, 5)
.AutoHeight()
[
// explain this is Control Rig window
// and what it is
SNew(STextBlock)
.TextStyle( FAppStyle::Get(), "Persona.RetargetManager.ImportantText" )
.Text(LOCTEXT("ControlRigMapping_Title", "Configure Control Rig Settings"))
]
+ SVerticalBox::Slot()
.Padding(5, 5)
.AutoHeight()
[
// explainint this is Control Rig window
// and what it is
SNew(STextBlock)
.AutoWrapText(true)
.ToolTip(IDocumentation::Get()->CreateToolTip(LOCTEXT("ControlRigMappingTooltip", "Add new Control Rig, and remap or edit mapping information."),
NULL,
DocLink,
TEXT("NodeMapping")))
.Font(FAppStyle::GetFontStyle(TEXT("Persona.RetargetManager.FilterFont")))
.Text(LOCTEXT("ControlRigMappingDescription", "You can add/delete Control Rig Mapping Configuration."))
]
// select and combo box
+ SVerticalBox::Slot()
.Padding(5)
.AutoHeight()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.Padding(2)
.AutoWidth()
[
// is this safe with uobject? - i.e. if this gets GC-ed?
SAssignNew(MappingOptionBox, SComboBox< TSharedPtr<class UNodeMappingContainer*> >)
.ContentPadding(FMargin(6.0f, 2.0f))
.OptionsSource(&MappingOptionBoxList)
.OnGenerateWidget(this, &SControlRigMappingWindow::HandleMappingOptionBoxGenerateWidget)
.OnSelectionChanged(this, &SControlRigMappingWindow::HandleMappingOptionBoxSelectionChanged)
[
SNew(STextBlock)
.Text(this, &SControlRigMappingWindow::HandleMappingOptionBoxContentText)
.Font(FAppStyle::GetFontStyle(TEXT("Persona.RetargetManager.FilterFont")))
]
]
// add new or delete button
+SHorizontalBox::Slot()
.Padding(2)
.AutoWidth()
[
SNew(SButton)
.OnClicked(FOnClicked::CreateSP(this, &SControlRigMappingWindow::OnAddNodeMappingButtonClicked))
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Text(LOCTEXT("AddNodeMappingButton_Label", "Add New"))
]
+SHorizontalBox::Slot()
.Padding(2)
.AutoWidth()
[
SNew(SButton)
.OnClicked(FOnClicked::CreateSP(this, &SControlRigMappingWindow::OnDeleteNodeMappingButtonClicked))
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Text(LOCTEXT("DeleteNodeMappingButton_Label", "Delete Current"))
]
+SHorizontalBox::Slot()
.Padding(2)
.AutoWidth()
[
SNew(SButton)
.OnClicked(FOnClicked::CreateSP(this, &SControlRigMappingWindow::OnRefreshNodeMappingButtonClicked))
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Text(LOCTEXT("RefreshNodeMappingButton_Label", "Refresh Mapping"))
]
]
+SVerticalBox::Slot()
.Padding(2, 5)
.AutoHeight()
[
SNew(SBox)
.MaxDesiredHeight(500)
.Content()
[
// add bone mapper window
SAssignNew(BoneMappingWidget, SBoneMappingBase, InOnPostUndo)
.OnBoneMappingChanged(this, &SControlRigMappingWindow::OnBoneMappingChanged)
.OnGetBoneMapping(this, &SControlRigMappingWindow::GetBoneMapping)
.OnCreateBoneMapping(this, &SControlRigMappingWindow::CreateBoneMappingList)
.OnGetReferenceSkeleton(this, &SControlRigMappingWindow::GetReferenceSkeleton)
]
]
+SVerticalBox::Slot()
.Padding(2, 5)
.AutoHeight()
[
SNew(SSeparator)
.Orientation(Orient_Horizontal)
]
];
RefreshList();
}
void SControlRigMappingWindow::PostUndo()
{
RefreshList();
}
TSharedRef<SWidget> SControlRigMappingWindow::HandleMappingOptionBoxGenerateWidget(TSharedPtr<class UNodeMappingContainer*> Item)
{
// @todo: create tooltip with path
return SNew(STextBlock)
.Text(FText::FromString(*(*Item)->GetDisplayName()))
.Font(FAppStyle::GetFontStyle(TEXT("Persona.RetargetManager.FilterFont")));
}
void SControlRigMappingWindow::HandleMappingOptionBoxSelectionChanged(TSharedPtr<class UNodeMappingContainer*> Item, ESelectInfo::Type SelectInfo)
{
for (int32 Index=0; Index <MappingOptionBoxList.Num(); ++Index)
{
if (MappingOptionBoxList[Index] == Item)
{
CurrentlySelectedIndex = Index;
break;
}
}
// @todo: refresh mapping window
MappingOptionBox->RefreshOptions();
BoneMappingWidget->RefreshBoneMappingList();
}
FText SControlRigMappingWindow::HandleMappingOptionBoxContentText() const
{
if (MappingOptionBoxList.IsValidIndex(CurrentlySelectedIndex))
{
return FText::FromString(*(*MappingOptionBoxList[CurrentlySelectedIndex])->GetDisplayName());
}
return LOCTEXT("ControlRigMappingWindow_NoneSelected", "None Selected. Create New.");
}
void SControlRigMappingWindow::AddNodeMapping(UBlueprint* NewSourceControlRig)
{
if ( ensureAlways(EditableSkeletalMeshPtr.IsValid()) )
{
// make sure it doesn't have it yet. If so, we warn them.
// @todo: It is possible we could support multi, but then we should give unique display name, and use that to identify.
// this all can get very messy, so for now, we just support one for each
// create new mapper object
USkeletalMesh* SkeletalMesh = EditableSkeletalMeshPtr.Get();
TArray<TObjectPtr<UNodeMappingContainer>>& NodeMappingData = SkeletalMesh->GetNodeMappingData();
for (int32 Index = 0; Index < NodeMappingData.Num(); ++Index)
{
if (NewSourceControlRig == NodeMappingData[Index]->GetSourceAsset())
{
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("ControlRigConfigAlreadyExists", "The same Control Rig configuration already exists in this mesh. Edit current existing setting."));
return;
}
}
{
const FScopedTransaction Transaction(LOCTEXT("ControlRigMapping_AddNew", "Add New Mapping"));
SkeletalMesh->Modify();
UNodeMappingContainer* NewMapperObject = NewObject<UNodeMappingContainer>(SkeletalMesh);
NewMapperObject->SetSourceAsset(NewSourceControlRig);
// set target asset as skeletalmesh
NewMapperObject->SetTargetAsset(SkeletalMesh);
// add default mapping, this will map default settings
NewMapperObject->AddDefaultMapping();
CurrentlySelectedIndex = SkeletalMesh->GetNodeMappingData().Add(NewMapperObject);
RefreshList();
}
}
}
FReply SControlRigMappingWindow::OnDeleteNodeMappingButtonClicked()
{
const FScopedTransaction Transaction(LOCTEXT("ControlRigMapping_Delete", "Delete Selected Mapping"));
// create new mapper object
USkeletalMesh* SkeletalMesh = EditableSkeletalMeshPtr.Get();
TArray<TObjectPtr<UNodeMappingContainer>>& NodeMappingData = SkeletalMesh->GetNodeMappingData();
if (NodeMappingData.IsValidIndex(CurrentlySelectedIndex))
{
SkeletalMesh->Modify();
NodeMappingData.RemoveAt(CurrentlySelectedIndex);
CurrentlySelectedIndex = (NodeMappingData.Num() > 0) ? 0 : INDEX_NONE;
RefreshList();
}
return FReply::Handled();
}
FReply SControlRigMappingWindow::OnRefreshNodeMappingButtonClicked()
{
// create new mapper object
USkeletalMesh* SkeletalMesh = EditableSkeletalMeshPtr.Get();
if (SkeletalMesh->GetNodeMappingData().IsValidIndex(CurrentlySelectedIndex))
{
const FScopedTransaction Transaction(LOCTEXT("ControlRigMapping_Refresh", "Refresh Node Mapping"));
UNodeMappingContainer* Container = GetCurrentBoneMappingContainer();
if (SkeletalMesh && Container)
{
SkeletalMesh->Modify();
Container->RefreshDataFromAssets();
}
}
return FReply::Handled();
}
void SControlRigMappingWindow::OnAddNodeMapping()
{
// show list of skeletalmeshes that they can choose from
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
FAssetPickerConfig AssetPickerConfig;
AssetPickerConfig.Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
AssetPickerConfig.Filter.bRecursiveClasses = true;
AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateSP(this, &SControlRigMappingWindow::OnAssetSelectedFromMeshPicker);
AssetPickerConfig.bAllowNullSelection = false;
AssetPickerConfig.InitialAssetViewType = EAssetViewType::Tile;
AssetPickerConfig.OnShouldFilterAsset = FOnShouldFilterAsset::CreateSP(this, &SControlRigMappingWindow::OnShouldFilterAnimAsset);
TSharedRef<SWidget> Widget = SNew(SBox)
.WidthOverride(384)
.HeightOverride(768)
[
SNew(SBorder)
.BorderBackgroundColor(FLinearColor(0.25f, 0.25f, 0.25f, 1.f))
.Padding(2)
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
.Padding(8)
[
ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig)
]
]
];
FSlateApplication::Get().PushMenu(
AsShared(),
FWidgetPath(),
Widget,
FSlateApplication::Get().GetCursorPos(),
FPopupTransitionEffect(FPopupTransitionEffect::TopMenu)
);
}
FReply SControlRigMappingWindow::OnAddNodeMappingButtonClicked()
{
OnAddNodeMapping();
return FReply::Handled();
}
void SControlRigMappingWindow::OnAssetSelectedFromMeshPicker(const FAssetData& AssetData)
{
// add to mapping now
AddNodeMapping(Cast<UBlueprint>(AssetData.GetAsset()));
FSlateApplication::Get().DismissAllMenus();
}
bool SControlRigMappingWindow::OnShouldFilterAnimAsset(const FAssetData& AssetData) const
{
FString ParentClassName;
if (AssetData.GetTagValue(FBlueprintTags::NativeParentClassPath, ParentClassName))
{
if (ParentClassName.IsEmpty() == false)
{
UClass* ParentClass = UClass::TryFindTypeSlow<UClass>(FPackageName::ExportTextPathToObjectPath(ParentClassName));
while (ParentClass && ParentClass != UObject::StaticClass())
{
if (ParentClass->ImplementsInterface(UNodeMappingProviderInterface::StaticClass()))
{
return false;
}
ParentClass = ParentClass->GetSuperClass();
}
}
}
return true;
}
void SControlRigMappingWindow::RefreshList()
{
// update list of mapping options
// @todo: have to make sure there is no duplicated name. If so, we'll have to create fake name
// for now, do path name
MappingOptionBoxList.Empty();
TArray<TObjectPtr<UNodeMappingContainer>>& NodeMappingData = EditableSkeletalMeshPtr->GetNodeMappingData();
for (int32 Index = 0; Index < NodeMappingData.Num(); ++Index)
{
class UNodeMappingContainer* MappingData = NodeMappingData[Index];
if (MappingData)
{
MappingOptionBoxList.Add(MakeShareable(new UNodeMappingContainer*(MappingData)));
}
else
{
// we have to add all index because we can't skip it
ensureAlwaysMsgf(false, TEXT("Invalid node mapping data exists. This will cause issue with later indices."));
}
}
CurrentlySelectedIndex = (MappingOptionBoxList.Num() > 0) ? 0 : INDEX_NONE;
MappingOptionBox->RefreshOptions();
BoneMappingWidget->RefreshBoneMappingList();
}
class UNodeMappingContainer* SControlRigMappingWindow::GetCurrentBoneMappingContainer() const
{
if (EditableSkeletalMeshPtr.IsValid())
{
USkeletalMesh* Mesh = EditableSkeletalMeshPtr.Get();
TArray<TObjectPtr<UNodeMappingContainer>>& NodeMappingData = Mesh->GetNodeMappingData();
if (NodeMappingData.IsValidIndex(CurrentlySelectedIndex))
{
return NodeMappingData[CurrentlySelectedIndex];
}
}
return nullptr;
}
void SControlRigMappingWindow::OnBoneMappingChanged(FName NodeName, FName BoneName)
{
// set mapping
USkeletalMesh* Mesh = EditableSkeletalMeshPtr.Get();
UNodeMappingContainer* Container = GetCurrentBoneMappingContainer();
if (Mesh && Container)
{
Container->AddMapping(NodeName, BoneName);
}
}
FName SControlRigMappingWindow::GetBoneMapping(FName NodeName)
{
UNodeMappingContainer* Container = GetCurrentBoneMappingContainer();
if (Container)
{
const FName* Target = Container->GetNodeMappingTable().Find(NodeName);;
return Target? (*Target) : NAME_None;
}
return NAME_None;
}
void SControlRigMappingWindow::CreateBoneMappingList(const FString& SearchText, TArray< TSharedPtr<FDisplayedBoneMappingInfo> >& BoneMappingList)
{
BoneMappingList.Empty();
const FReferenceSkeleton RefSkeleton = EditableSkeletalMeshPtr.Get()->GetRefSkeleton();
UNodeMappingContainer* Container = GetCurrentBoneMappingContainer();
if (Container)
{
bool bDoFiltering = !SearchText.IsEmpty();
const TMap<FName, FNodeItem>& SourceItems = Container->GetSourceItems();
const TMap<FName, FName>& SourceToTarget = Container->GetNodeMappingTable();
if ( SourceItems.Num() > 0 )
{
for ( auto Iter = SourceItems.CreateConstIterator() ; Iter; ++Iter )
{
const FName& Name = Iter.Key();
const FString& DisplayName = Name.ToString();
const FName* BoneName = SourceToTarget.Find(Name);
if (bDoFiltering)
{
// make sure it doesn't fit any of them
if (!DisplayName.Contains(SearchText) && (BoneName && !(*BoneName).ToString().Contains(SearchText)))
{
continue; // Skip items that don't match our filter
}
}
TSharedRef<FDisplayedBoneMappingInfo> Info = FDisplayedBoneMappingInfo::Make(Name, DisplayName);
BoneMappingList.Add(Info);
}
}
}
}
const struct FReferenceSkeleton& SControlRigMappingWindow::GetReferenceSkeleton() const
{
static FReferenceSkeleton DummySkeleton;
return (EditableSkeletalMeshPtr.IsValid())? EditableSkeletalMeshPtr.Get()->GetRefSkeleton() : DummySkeleton;
}
#undef LOCTEXT_NAMESPACE