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

344 lines
9.2 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "BoneSelectionWidget.h"
#include "Styling/AppStyle.h"
#include "DetailLayoutBuilder.h"
#include "Widgets/Layout/SSeparator.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SSearchBox.h"
#include "IEditableSkeleton.h"
#include "Engine/SkeletalMeshSocket.h"
#define LOCTEXT_NAMESPACE "SBoneSelectionWidget"
/////////////////////////////////////////////////////
void SBoneTreeMenu::Construct(const FArguments& InArgs)
{
OnSelectionChangedDelegate = InArgs._OnBoneSelectionChanged;
OnGetReferenceSkeletonDelegate = InArgs._OnGetReferenceSkeleton;
OnGetSocketListDelegate = InArgs._OnGetSocketList;
bShowVirtualBones = InArgs._bShowVirtualBones;
bShowSocket = InArgs._bShowSocket;
bShowNone = InArgs._bShowNone;
FText TitleToUse = !InArgs._Title.IsEmpty() ? InArgs._Title : LOCTEXT("BonePickerTitle", "Select...");
SAssignNew(TreeView, STreeView<TSharedPtr<FBoneNameInfo>>)
.TreeItemsSource(&SkeletonTreeInfo)
.OnGenerateRow(this, &SBoneTreeMenu::MakeTreeRowWidget)
.OnGetChildren(this, &SBoneTreeMenu::GetChildrenForInfo)
.OnSelectionChanged(this, &SBoneTreeMenu::OnSelectionChanged)
.SelectionMode(ESelectionMode::Single);
RebuildBoneList(InArgs._SelectedBone);
ChildSlot
[
SNew(SBorder)
.Padding(6.f)
.BorderImage(FAppStyle::GetBrush("NoBorder"))
.Content()
[
SNew(SBox)
.WidthOverride(300.f)
.HeightOverride(512.f)
.Content()
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(STextBlock)
.Font(FAppStyle::GetFontStyle("BoldFont"))
.Text(TitleToUse)
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SSeparator)
.SeparatorImage(FAppStyle::GetBrush("Menu.Separator"))
.Orientation(Orient_Horizontal)
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SAssignNew(FilterTextWidget, SSearchBox)
.SelectAllTextWhenFocused(true)
.OnTextChanged(this, &SBoneTreeMenu::OnFilterTextChanged)
.HintText(NSLOCTEXT("BonePicker", "Search", "Search..."))
]
+ SVerticalBox::Slot()
[
TreeView->AsShared()
]
]
]
];
}
TSharedPtr<SWidget> SBoneTreeMenu::GetFilterTextWidget()
{
return FilterTextWidget;
}
TSharedRef<ITableRow> SBoneTreeMenu::MakeTreeRowWidget(TSharedPtr<FBoneNameInfo> InInfo, const TSharedRef<STableViewBase>& OwnerTable)
{
return SNew(STableRow<TSharedPtr<FBoneNameInfo>>, OwnerTable)
.Content()
[
SNew(STextBlock)
.HighlightText(FilterText)
.Text(FText::FromName(InInfo->BoneName))
];
}
void SBoneTreeMenu::GetChildrenForInfo(TSharedPtr<FBoneNameInfo> InInfo, TArray< TSharedPtr<FBoneNameInfo> >& OutChildren)
{
OutChildren = InInfo->Children;
}
void SBoneTreeMenu::OnFilterTextChanged(const FText& InFilterText)
{
FilterText = InFilterText;
RebuildBoneList(NAME_None);
}
void SBoneTreeMenu::OnSelectionChanged(TSharedPtr<SBoneTreeMenu::FBoneNameInfo> BoneInfo, ESelectInfo::Type SelectInfo)
{
//Because we recreate all our items on tree refresh we will get a spurious null selection event initially.
if (BoneInfo.IsValid() && SelectInfo == ESelectInfo::OnMouseClick)
{
SelectBone(BoneInfo);
}
}
void SBoneTreeMenu::SelectBone(TSharedPtr<SBoneTreeMenu::FBoneNameInfo> BoneInfo)
{
OnSelectionChangedDelegate.ExecuteIfBound(BoneInfo->BoneName);
}
FReply SBoneTreeMenu::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
{
if(InKeyEvent.GetKey() == EKeys::Enter)
{
TArray<TSharedPtr<SBoneTreeMenu::FBoneNameInfo>> SelectedItems = TreeView->GetSelectedItems();
if(SelectedItems.Num() > 0)
{
SelectBone(SelectedItems[0]);
return FReply::Handled();
}
}
return FReply::Unhandled();
}
void SBoneTreeMenu::RebuildBoneList(const FName& SelectedBone)
{
SkeletonTreeInfo.Empty();
SkeletonTreeInfoFlat.Empty();
if (bShowNone)
{
TSharedRef<FBoneNameInfo> NoneBoneInfo = MakeShared<FBoneNameInfo>(NAME_None);
SkeletonTreeInfo.Add(NoneBoneInfo);
SkeletonTreeInfoFlat.Add(NoneBoneInfo);
}
if (ensure(OnGetReferenceSkeletonDelegate.IsBound()))
{
const FReferenceSkeleton& RefSkeleton = OnGetReferenceSkeletonDelegate.Execute();
const int32 MaxBone = bShowVirtualBones ? RefSkeleton.GetNum() : RefSkeleton.GetRawBoneNum();
for (int32 BoneIdx = 0; BoneIdx < MaxBone; ++BoneIdx)
{
const FName BoneName = RefSkeleton.GetBoneName(BoneIdx);
TSharedRef<FBoneNameInfo> BoneInfo = MakeShareable(new FBoneNameInfo(BoneName));
// Filter if Necessary
if (!FilterText.IsEmpty() && !BoneInfo->BoneName.ToString().Contains(FilterText.ToString()))
{
continue;
}
int32 ParentIdx = RefSkeleton.GetParentIndex(BoneIdx);
bool bAddToParent = false;
if (ParentIdx != INDEX_NONE && FilterText.IsEmpty())
{
// We have a parent, search for it in the flat list
FName ParentName = RefSkeleton.GetBoneName(ParentIdx);
for (int32 FlatListIdx = 0; FlatListIdx < SkeletonTreeInfoFlat.Num(); ++FlatListIdx)
{
TSharedPtr<FBoneNameInfo> InfoEntry = SkeletonTreeInfoFlat[FlatListIdx];
if (InfoEntry->BoneName == ParentName)
{
bAddToParent = true;
ParentIdx = FlatListIdx;
break;
}
}
if (bAddToParent)
{
SkeletonTreeInfoFlat[ParentIdx]->Children.Add(BoneInfo);
}
else
{
SkeletonTreeInfo.Add(BoneInfo);
}
}
else
{
SkeletonTreeInfo.Add(BoneInfo);
}
SkeletonTreeInfoFlat.Add(BoneInfo);
TreeView->SetItemExpansion(BoneInfo, true);
if (BoneName == SelectedBone)
{
TreeView->SetItemSelection(BoneInfo, true);
TreeView->RequestScrollIntoView(BoneInfo);
}
}
}
if (bShowSocket && ensure(OnGetSocketListDelegate.IsBound()))
{
const TArray<class USkeletalMeshSocket*>& Sockets = OnGetSocketListDelegate.Execute();
const int32 MaxSockets = Sockets.Num();
for (int32 SocketIdx = 0; SocketIdx < MaxSockets; ++SocketIdx)
{
const USkeletalMeshSocket* Socket = Sockets[SocketIdx];
const FName SocketName = Socket->SocketName;
TSharedRef<FBoneNameInfo> BoneInfo = MakeShareable(new FBoneNameInfo(SocketName));
// Filter if Necessary
if (!FilterText.IsEmpty() && !BoneInfo->BoneName.ToString().Contains(FilterText.ToString()))
{
continue;
}
const FName ParentBoneName = Socket->BoneName;
if (FilterText.IsEmpty())
{
for (int32 FlatListIdx = 0; FlatListIdx < SkeletonTreeInfoFlat.Num(); ++FlatListIdx)
{
TSharedPtr<FBoneNameInfo> InfoEntry = SkeletonTreeInfoFlat[FlatListIdx];
if (InfoEntry->BoneName == ParentBoneName)
{
SkeletonTreeInfoFlat[FlatListIdx]->Children.Add(BoneInfo);
break;
}
}
}
else
{
SkeletonTreeInfo.Add(BoneInfo);
}
TreeView->SetItemExpansion(BoneInfo, true);
if (SocketName == SelectedBone)
{
TreeView->SetItemSelection(BoneInfo, true);
TreeView->RequestScrollIntoView(BoneInfo);
}
}
}
TreeView->RequestTreeRefresh();
}
/////////////////////////////////////////////////////
void SBoneSelectionWidget::Construct(const FArguments& InArgs)
{
OnBoneSelectionChanged = InArgs._OnBoneSelectionChanged;
OnGetSelectedBone = InArgs._OnGetSelectedBone;
OnGetReferenceSkeleton = InArgs._OnGetReferenceSkeleton;
OnGetSocketList = InArgs._OnGetSocketList;
bShowSocket = InArgs._bShowSocket;
bShowVirtualBones = InArgs._bShowVirtualBones;
bShowNone = InArgs._bShowNone;
SuppliedToolTip = InArgs._ToolTipText.Get();
this->ChildSlot
[
SAssignNew(BonePickerButton, SComboButton)
.OnGetMenuContent(FOnGetContent::CreateSP(this, &SBoneSelectionWidget::CreateSkeletonWidgetMenu))
.ContentPadding(FMargin(4.0f, 2.0f, 4.0f, 2.0f))
.ButtonContent()
[
SNew(STextBlock)
.Text(this, &SBoneSelectionWidget::GetCurrentBoneName)
.Font(IDetailLayoutBuilder::GetDetailFont())
.ToolTipText(this, &SBoneSelectionWidget::GetFinalToolTip)
]
];
}
TSharedRef<SWidget> SBoneSelectionWidget::CreateSkeletonWidgetMenu()
{
bool bMultipleValues = false;
FName CurrentBoneName;
if (OnGetSelectedBone.IsBound())
{
CurrentBoneName = OnGetSelectedBone.Execute(bMultipleValues);
}
TSharedRef<SBoneTreeMenu> MenuWidget = SNew(SBoneTreeMenu)
.OnBoneSelectionChanged(this, &SBoneSelectionWidget::OnSelectionChanged)
.OnGetReferenceSkeleton(OnGetReferenceSkeleton)
.OnGetSocketList(OnGetSocketList)
.bShowSocket(bShowSocket)
.bShowNone(bShowNone)
.SelectedBone(CurrentBoneName)
.bShowVirtualBones(bShowVirtualBones);
BonePickerButton->SetMenuContentWidgetToFocus(MenuWidget->GetFilterTextWidget());
return MenuWidget;
}
void SBoneSelectionWidget::OnSelectionChanged(FName BoneName)
{
//Because we recreate all our items on tree refresh we will get a spurious null selection event initially.
if (OnBoneSelectionChanged.IsBound())
{
OnBoneSelectionChanged.Execute(BoneName);
}
BonePickerButton->SetIsOpen(false);
}
FText SBoneSelectionWidget::GetCurrentBoneName() const
{
if(OnGetSelectedBone.IsBound())
{
bool bMultipleValues = false;
FName Name = OnGetSelectedBone.Execute(bMultipleValues);
if(bMultipleValues)
{
return LOCTEXT("MultipleValues", "Multiple Values");
}
else
{
return FText::FromName(Name);
}
}
// @todo implement default solution?
return FText::GetEmpty();
}
FText SBoneSelectionWidget::GetFinalToolTip() const
{
return FText::Format(LOCTEXT("BoneClickToolTip", "Bone:{0}\n\n{1}\nClick to choose a different bone"), GetCurrentBoneName(), SuppliedToolTip);
}
#undef LOCTEXT_NAMESPACE