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

561 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SBlendProfilePicker.h"
#include "Fonts/SlateFontInfo.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SOverlay.h"
#include "Layout/WidgetPath.h"
#include "SlateOptMacros.h"
#include "Framework/Application/MenuStack.h"
#include "Framework/Application/SlateApplication.h"
#include "Textures/SlateIcon.h"
#include "Framework/Commands/UIAction.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Text/STextBlock.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SComboButton.h"
#include "Styling/AppStyle.h"
#include "Editor/EditorEngine.h"
#include "EngineGlobals.h"
#include "Animation/BlendProfile.h"
#include "Widgets/Input/STextEntryPopup.h"
#include "PropertyHandle.h"
#include "ScopedTransaction.h"
#define LOCTEXT_NAMESPACE "BlendProfilePicker"
namespace BlendProfilePickerNames
{
static const FText BlendProfileModeName = LOCTEXT("Blend Profile Mode Name", "Blend Profile");
static const FText BlendMaskModeName = LOCTEXT("Blend Mask Mode Name", "Blend Mask");
const FText& GetNameForMode(EBlendProfileMode InMode)
{
return (InMode == EBlendProfileMode::BlendMask) ? BlendProfilePickerNames::BlendMaskModeName : BlendProfilePickerNames::BlendProfileModeName;
}
}
class SBlendProfileMenuEntry : public SCompoundWidget
{
public:
DECLARE_DELEGATE_OneParam(FBlendProfileModeChanged, EBlendProfileMode);
SLATE_BEGIN_ARGS(SBlendProfileMenuEntry){}
SLATE_ARGUMENT( FText, LabelOverride )
/** Called to when an entry is clicked */
SLATE_EVENT( FExecuteAction, OnOpenClickedDelegate )
/** Called to when the button remove an entry is clicked */
SLATE_EVENT( FExecuteAction, OnRemoveClickedDelegate )
SLATE_EVENT(FBlendProfileModeChanged, OnProfileModeChangedDelegate)
/** Whether to show the remove button */
SLATE_ARGUMENT(bool, AllowModify)
SLATE_ARGUMENT(TWeakObjectPtr<UBlendProfile>, BlendProfile)
SLATE_END_ARGS()
void Construct( const FArguments& InArgs )
{
const FText DisplayName = InArgs._LabelOverride;
OnOpenClickedDelegate = InArgs._OnOpenClickedDelegate;
OnRemoveClickedDelegate = InArgs._OnRemoveClickedDelegate;
OnProfileModeChangedDelegate = InArgs._OnProfileModeChangedDelegate;
BlendProfile = InArgs._BlendProfile;
FSlateFontInfo MenuEntryFont = FAppStyle::GetFontStyle( "Menu.Label.Font" );
TSharedPtr<SHorizontalBox> HorizontalBox;
EBlendProfileMode BlendProfileMode = GetProfileMode();
ChildSlot
[
SNew(SButton)
.ButtonStyle( FAppStyle::Get(), "Menu.Button" )
.ForegroundColor( TAttribute<FSlateColor>::Create( TAttribute<FSlateColor>::FGetter::CreateRaw( this, &SBlendProfileMenuEntry::InvertOnHover ) ) )
.ToolTipText(LOCTEXT("OpenBlendProfileToolTip", "Select this profile for editing."))
.OnClicked(this, &SBlendProfileMenuEntry::OnOpen)
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
.ContentPadding( FMargin(4.0, 2.0) )
[
SNew(SOverlay)
+SOverlay::Slot()
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
[
SAssignNew(HorizontalBox, SHorizontalBox)
+SHorizontalBox::Slot()
.Padding( FMargin( 12.0, 0.0 ) )
.VAlign(VAlign_Center)
.HAlign(HAlign_Fill)
[
SNew(STextBlock)
.Font(MenuEntryFont)
.ColorAndOpacity(TAttribute<FSlateColor>::Create(TAttribute<FSlateColor>::FGetter::CreateRaw(this, &SBlendProfileMenuEntry::InvertOnHover)))
.Text(DisplayName)
]
]
]
];
if(InArgs._AllowModify)
{
HorizontalBox->AddSlot()
.Padding(FMargin(8.0, 0.0))
.VAlign(VAlign_Center)
.HAlign(HAlign_Right)
.AutoWidth()
[
SNew(SComboButton)
.ButtonColorAndOpacity(FLinearColor::Transparent)
.ButtonStyle(FAppStyle::Get(), "PropertyEditor.AssetComboStyle")
.HasDownArrow(false)
.ForegroundColor(FAppStyle::GetColor("PropertyEditor.AssetName.ColorAndOpacity"))
.ContentPadding(2.0f)
.ToolTipText(FText::Format(LOCTEXT("ModifyBlendProfileToolTipFmt", "Modify {0}"), DisplayName))
.MenuPlacement(MenuPlacement_MenuRight)
.OnGetMenuContent(this, &SBlendProfileMenuEntry::GetMenuContent)
.ButtonContent()
[
SNew(SImage)
.ColorAndOpacity(FSlateColor::UseForeground())
.Image(FAppStyle::Get().GetBrush("Icons.Settings"))
]
];
}
}
void OnProfileModeSelected(EBlendProfileMode InMode)
{
OnProfileModeChangedDelegate.ExecuteIfBound(InMode);
}
EBlendProfileMode GetProfileMode()
{
if (BlendProfile.IsValid())
{
return BlendProfile->Mode;
}
return EBlendProfileMode::WeightFactor;
}
bool IsProfileModeSelected(EBlendProfileMode InMode)
{
if (BlendProfile.IsValid())
{
return BlendProfile->Mode == InMode;
}
return false;
}
TSharedRef<SWidget> GetMenuContent()
{
FMenuBuilder MenuBuilder(false, nullptr, TSharedPtr<FExtender>(), true);
EBlendProfileMode BlendProfileMode = GetProfileMode();
if (BlendProfileMode != EBlendProfileMode::BlendMask)
{
MenuBuilder.BeginSection(NAME_None, LOCTEXT("BlendProfileMode", "Mode"));
{
UEnum* ModeEnum = StaticEnum<EBlendProfileMode>();
check(ModeEnum);
// Last enum entry is _MAX
int32 NumEnums = ModeEnum->NumEnums() - 1;
static const TCHAR* HiddenMeta = TEXT("Hidden");
for (int32 EnumIndex = 0; EnumIndex < NumEnums; ++EnumIndex)
{
if (!ModeEnum->HasMetaData(HiddenMeta, EnumIndex))
{
EBlendProfileMode IndexMode = (EBlendProfileMode)ModeEnum->GetValueByIndex(EnumIndex);
MenuBuilder.AddMenuEntry(
ModeEnum->GetDisplayNameTextByIndex(EnumIndex),
ModeEnum->GetToolTipTextByIndex(EnumIndex),
FSlateIcon(),
FUIAction(
FExecuteAction::CreateSP(this, &SBlendProfileMenuEntry::OnProfileModeSelected, IndexMode),
FCanExecuteAction(),
FIsActionChecked::CreateSP(this, &SBlendProfileMenuEntry::IsProfileModeSelected, IndexMode)
),
NAME_None,
EUserInterfaceActionType::RadioButton);
}
}
}
MenuBuilder.EndSection();
}
MenuBuilder.BeginSection(NAME_None, LOCTEXT("BlendProfileActions", "Actions"));
{
const FText& ModeName = BlendProfilePickerNames::GetNameForMode(BlendProfileMode);
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("RemoveBlendProfile", "Remove {0}"), ModeName),
FText::Format(LOCTEXT("RemoveBlendProfile_ToolTip", "Remove this {0} from the skeleton"), ModeName),
FSlateIcon(),
FUIAction(FExecuteAction::CreateLambda([this]()
{
OnRemoveClickedDelegate.ExecuteIfBound();
FSlateApplication::Get().DismissAllMenus();
})));
}
MenuBuilder.EndSection();
return MenuBuilder.MakeWidget();
}
FReply OnOpen()
{
OnOpenClickedDelegate.ExecuteIfBound();
FSlateApplication::Get().DismissAllMenus();
return FReply::Handled();
}
private:
FSlateColor InvertOnHover() const
{
if ( this->IsHovered() )
{
return FLinearColor::Black;
}
else
{
return FSlateColor::UseForeground();
}
}
FExecuteAction OnOpenClickedDelegate;
FExecuteAction OnRemoveClickedDelegate;
FBlendProfileModeChanged OnProfileModeChangedDelegate;
TWeakObjectPtr<UBlendProfile> BlendProfile;
};
//////////////////////////////////////////////////////////////////////////
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SBlendProfilePicker::Construct(const FArguments& InArgs, TSharedRef<class IEditableSkeleton> InEditableSkeleton)
{
bShowNewOption = InArgs._AllowNew;
bShowClearOption = InArgs._AllowClear;
bIsStandalone = InArgs._Standalone;
EditableSkeleton = InEditableSkeleton;
SupportedBlendProfileModes = InArgs._SupportedBlendProfileModes;
bAllowModify = InArgs._AllowModify;
UEditorEngine* Editor = Cast<UEditorEngine>(GEngine);
if (Editor != nullptr)
{
Editor->RegisterForUndo(this);
}
PropertyHandle = InArgs._PropertyHandle;
if (PropertyHandle.IsValid())
{
FSimpleDelegate OnModeChanged = FSimpleDelegate::CreateLambda([this]
{
UObject * PropertyValue = nullptr;
PropertyHandle->GetValue(PropertyValue);
UBlendProfile * CurrentProfile = Cast<UBlendProfile>(PropertyValue);
// Avoid broadcasting to not double call FBlendProfileCustomization::OnBlendProfileChanged
SetSelectedProfile(CurrentProfile, false);
});
// Sometimes, the property value changes externally (not through the blend profile picker i.e. Reset to default). Must notify the blend profile picker
PropertyHandle->SetOnPropertyValueChanged(OnModeChanged);
}
if(InArgs._InitialProfile != nullptr && InEditableSkeleton->GetBlendProfiles().Contains(InArgs._InitialProfile))
{
SelectedProfileName = InArgs._InitialProfile->GetFName();
}
else
{
SelectedProfileName = NAME_None;
}
BlendProfileSelectedDelegate = InArgs._OnBlendProfileSelected;
TSharedRef<SWidget> TextBlock = SNew(STextBlock)
.TextStyle(FAppStyle::Get(), "PropertyEditor.AssetClass")
.Font(FAppStyle::GetFontStyle("PropertyWindow.NormalFont"))
.Text(this, &SBlendProfilePicker::GetSelectedProfileName);
TSharedPtr<SWidget> ButtonContent;
if (bIsStandalone)
{
ButtonContent = SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 2.0f)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(FAppStyle::GetBrush("SkeletonTree.BlendProfile"))
]
+SHorizontalBox::Slot()
.Padding(2.0f, 0, 8.0f, 0)
.VAlign(VAlign_Center)
[
TextBlock
];
}
else
{
ButtonContent = TextBlock;
}
ChildSlot
[
SNew(SComboButton)
.ButtonStyle(FAppStyle::Get(), "PropertyEditor.AssetComboStyle")
.ForegroundColor(FAppStyle::GetColor("PropertyEditor.AssetName.ColorAndOpacity"))
.ContentPadding(2.0f)
.OnGetMenuContent(this, &SBlendProfilePicker::GetMenuContent)
.ButtonContent()
[
ButtonContent.ToSharedRef()
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
SBlendProfilePicker::~SBlendProfilePicker()
{
UEditorEngine* Editor = Cast<UEditorEngine>(GEngine);
if (Editor != nullptr)
{
Editor->UnregisterForUndo(this);
}
}
FText SBlendProfilePicker::GetSelectedProfileName() const
{
UBlendProfile* SelectedProfile = EditableSkeleton->GetBlendProfile(SelectedProfileName);
if(SelectedProfile)
{
if (bIsStandalone)
{
return FText::Format(FText(LOCTEXT("SelectedNameEntryStandalone", "{0}: {1}")), BlendProfilePickerNames::GetNameForMode(SelectedProfile->Mode), FText::FromName(SelectedProfileName));
}
else
{
return FText::Format(FText(LOCTEXT("SelectedNameEntry", "{0}")), FText::FromName(SelectedProfileName));
}
}
if (bIsStandalone)
{
return FText(LOCTEXT("NoSelectionEntryStandalone", "Blend Profile/Mask: None"));
}
else
{
return FText(LOCTEXT("NoSelectionEntry", "None"));
}
}
TSharedRef<SWidget> SBlendProfilePicker::GetMenuContent()
{
FMenuBuilder MenuBuilder(true, nullptr);
const bool bHasSettingsSection = bShowNewOption || bShowClearOption;
if(bHasSettingsSection)
{
MenuBuilder.BeginSection(NAME_None, LOCTEXT("MenuSettings", "Settings"));
{
if(bShowNewOption)
{
MenuBuilder.AddMenuEntry(
LOCTEXT("CreateNewBlendProfile", "Create New Blend Profile"),
LOCTEXT("CreateNewBlendProfile_ToolTip", "Creates a new blend profile inside the skeleton."),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SBlendProfilePicker::OnCreateNewProfile, EBlendProfileMode::TimeFactor))); //Default blend profiles to time factor
MenuBuilder.AddMenuEntry(
LOCTEXT("CreateNewBlendMask", "Create New Blend Mask"),
LOCTEXT("CreateNewBlendMask_ToolTip", "Creates a new blend mask inside the skeleton."),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SBlendProfilePicker::OnCreateNewProfile, EBlendProfileMode::BlendMask)));
}
if(bShowClearOption)
{
MenuBuilder.AddMenuEntry(
LOCTEXT("Clear", "Clear"),
LOCTEXT("Clear_ToolTip", "Clear the selected blend profile."),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SBlendProfilePicker::OnClearSelection)));
}
}
MenuBuilder.EndSection();
}
const static FText BlendProfileHeaders[] =
{
LOCTEXT("BlendProfilesTimeBased", "Blend Profiles - Time"),
LOCTEXT("BlendProfilesWeightBased", "Blend Profiles - Weight"),
LOCTEXT("BlendMasks", "Blend Masks")
};
UEnum* ModeEnum = StaticEnum<EBlendProfileMode>();
check(ModeEnum);
// Last enum entry is _MAX
int32 NumEnums = ModeEnum->NumEnums() - 1;
TArray<TArray<UBlendProfile*>> BlendProfilesFiltered;
BlendProfilesFiltered.SetNum(NumEnums);
// Build a filtered profile list by mode
for (UBlendProfile* Profile : EditableSkeleton->GetBlendProfiles())
{
if (Profile)
{
BlendProfilesFiltered[ModeEnum->GetIndexByValue((int64)Profile->GetMode())].Add(Profile);
}
}
const bool bSupportsBlendMasks = EnumHasAnyFlags(SupportedBlendProfileModes, EBlendProfilePickerMode::BlendMask);
const bool bSupportsBlendProfiles = EnumHasAnyFlags(SupportedBlendProfileModes, EBlendProfilePickerMode::BlendProfile);
for (int32 ModeIndex = 0; ModeIndex < NumEnums; ++ModeIndex)
{
EBlendProfileMode IndexMode = (EBlendProfileMode)ModeEnum->GetValueByIndex(ModeIndex);
if ((IndexMode == EBlendProfileMode::BlendMask && bSupportsBlendMasks)
|| (IndexMode != EBlendProfileMode::BlendMask && bSupportsBlendProfiles))
{
// Note: Section won't get populated if there are no available items for this mode type
MenuBuilder.BeginSection(NAME_None, BlendProfileHeaders[ModeIndex]);
for (UBlendProfile* Profile : BlendProfilesFiltered[ModeIndex])
{
if (IndexMode == Profile->GetMode())
{
MenuBuilder.AddWidget(
SNew(SBlendProfileMenuEntry)
.LabelOverride(FText::FromString(Profile->GetName()))
.OnOpenClickedDelegate(FExecuteAction::CreateSP(this, &SBlendProfilePicker::OnProfileSelected, Profile->GetFName()))
.OnRemoveClickedDelegate(FExecuteAction::CreateSP(this, &SBlendProfilePicker::OnProfileRemoved, Profile->GetFName()))
.OnProfileModeChangedDelegate(SBlendProfileMenuEntry::FBlendProfileModeChanged::CreateSP(this, &SBlendProfilePicker::OnProfileModeChanged, Profile->GetFName()))
.BlendProfile(MakeWeakObjectPtr(Profile))
.AllowModify(bAllowModify),
FText(),
true
);
}
}
}
MenuBuilder.EndSection();
}
return MenuBuilder.MakeWidget();
}
void SBlendProfilePicker::OnProfileSelected(FName InBlendProfileName)
{
SelectedProfileName = InBlendProfileName;
BlendProfileSelectedDelegate.ExecuteIfBound(EditableSkeleton->GetBlendProfile(SelectedProfileName));
}
void SBlendProfilePicker::OnClearSelection()
{
SelectedProfileName = NAME_None;
BlendProfileSelectedDelegate.ExecuteIfBound(nullptr);
}
void SBlendProfilePicker::OnProfileRemoved(FName InBlendProfileName)
{
EditableSkeleton->RemoveBlendProfile(EditableSkeleton->GetBlendProfile(InBlendProfileName));
SelectedProfileName = NAME_None;
BlendProfileSelectedDelegate.ExecuteIfBound(nullptr);
}
void SBlendProfilePicker::OnProfileModeChanged(EBlendProfileMode ProfileMode, FName InBlendProfileName)
{
EditableSkeleton->SetBlendProfileMode(InBlendProfileName, ProfileMode);
}
void SBlendProfilePicker::OnCreateNewProfile(EBlendProfileMode InMode)
{
TSharedRef<STextEntryPopup> TextEntry = SNew(STextEntryPopup)
.Label(LOCTEXT("NewProfileName", "Profile Name"))
.OnTextCommitted(this, &SBlendProfilePicker::OnCreateNewProfileComitted, InMode)
.OnVerifyTextChanged_Lambda([](const FText& InNewText, FText& OutErrorMessage) -> bool
{
return FName::IsValidXName(InNewText.ToString(), INVALID_OBJECTNAME_CHARACTERS INVALID_LONGPACKAGE_CHARACTERS, &OutErrorMessage);
});
FSlateApplication::Get().PushMenu(
AsShared(),
FWidgetPath(),
TextEntry,
FSlateApplication::Get().GetCursorPos(),
FPopupTransitionEffect(FPopupTransitionEffect::TypeInPopup));
}
void SBlendProfilePicker::OnCreateNewProfileComitted(const FText& NewName, ETextCommit::Type CommitType, EBlendProfileMode InMode)
{
FSlateApplication::Get().DismissAllMenus();
if((CommitType == ETextCommit::OnEnter || CommitType == ETextCommit::OnUserMovedFocus) && EditableSkeleton.IsValid())
{
FScopedTransaction Transaction(LOCTEXT("Trans_NewProfile", "Create new blend profile."));
FName NameToUse = FName(*NewName.ToString());
// Only create if we don't have a matching profile
if(UBlendProfile* FoundProfile = EditableSkeleton->GetBlendProfile(NameToUse))
{
OnProfileSelected(FoundProfile->GetFName());
}
else if(UBlendProfile* NewProfile = EditableSkeleton->CreateNewBlendProfile(NameToUse))
{
// Set our initial blend profile mode. Blend masks can't change this.
NewProfile->Mode = InMode;
OnProfileSelected(NewProfile->GetFName());
}
}
}
void SBlendProfilePicker::SetSelectedProfile(UBlendProfile* InProfile, bool bBroadcast /*= true*/)
{
if(EditableSkeleton->GetBlendProfiles().Contains(InProfile))
{
SelectedProfileName = InProfile->GetFName();
if(bBroadcast)
{
BlendProfileSelectedDelegate.ExecuteIfBound(InProfile);
}
}
else if(!InProfile)
{
OnClearSelection();
}
}
UBlendProfile* const SBlendProfilePicker::GetSelectedBlendProfile() const
{
return EditableSkeleton->GetBlendProfile(SelectedProfileName);
}
FName SBlendProfilePicker::GetSelectedBlendProfileName() const
{
UBlendProfile* SelectedProfile = EditableSkeleton->GetBlendProfile(SelectedProfileName);
return SelectedProfile ? SelectedProfile->GetFName() : NAME_None;
}
void SBlendProfilePicker::PostUndo(bool bSuccess)
{
BlendProfileSelectedDelegate.ExecuteIfBound(EditableSkeleton->GetBlendProfile(SelectedProfileName));
}
void SBlendProfilePicker::PostRedo(bool bSuccess)
{
BlendProfileSelectedDelegate.ExecuteIfBound(EditableSkeleton->GetBlendProfile(SelectedProfileName));
}
#undef LOCTEXT_NAMESPACE