// 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, 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 HorizontalBox; EBlendProfileMode BlendProfileMode = GetProfileMode(); ChildSlot [ SNew(SButton) .ButtonStyle( FAppStyle::Get(), "Menu.Button" ) .ForegroundColor( TAttribute::Create( TAttribute::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::Create(TAttribute::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 GetMenuContent() { FMenuBuilder MenuBuilder(false, nullptr, TSharedPtr(), true); EBlendProfileMode BlendProfileMode = GetProfileMode(); if (BlendProfileMode != EBlendProfileMode::BlendMask) { MenuBuilder.BeginSection(NAME_None, LOCTEXT("BlendProfileMode", "Mode")); { UEnum* ModeEnum = StaticEnum(); 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 BlendProfile; }; ////////////////////////////////////////////////////////////////////////// BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SBlendProfilePicker::Construct(const FArguments& InArgs, TSharedRef InEditableSkeleton) { bShowNewOption = InArgs._AllowNew; bShowClearOption = InArgs._AllowClear; bIsStandalone = InArgs._Standalone; EditableSkeleton = InEditableSkeleton; SupportedBlendProfileModes = InArgs._SupportedBlendProfileModes; bAllowModify = InArgs._AllowModify; UEditorEngine* Editor = Cast(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(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 TextBlock = SNew(STextBlock) .TextStyle(FAppStyle::Get(), "PropertyEditor.AssetClass") .Font(FAppStyle::GetFontStyle("PropertyWindow.NormalFont")) .Text(this, &SBlendProfilePicker::GetSelectedProfileName); TSharedPtr 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(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 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(); check(ModeEnum); // Last enum entry is _MAX int32 NumEnums = ModeEnum->NumEnums() - 1; TArray> 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 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