// Copyright Epic Games, Inc. All Rights Reserved. #include "PhysicsAssetDetailsCustomization.h" #include "Widgets/SCompoundWidget.h" #include "PhysicsEngine/PhysicsAsset.h" #include "DetailLayoutBuilder.h" #include "DetailCategoryBuilder.h" #include "IDetailPropertyRow.h" #include "DetailWidgetRow.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "PhysicsAssetEditorActions.h" #include "PhysicsAssetEditor.h" #include "PhysicsEngine/PhysicsConstraintTemplate.h" #include "PhysicsAssetEditorSkeletalMeshComponent.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "EditorFontGlyphs.h" #include "Widgets/Layout/SUniformGridPanel.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Input/SButton.h" #include "Widgets/Images/SImage.h" #include "ScopedTransaction.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SEditableTextBox.h" #include "PropertyHandle.h" #include "PhysicsAssetEditorActions.h" #define LOCTEXT_NAMESPACE "PhysicsAssetDetailsCustomization" TSharedRef FPhysicsAssetDetailsCustomization::MakeInstance(TWeakPtr InPhysicsAssetEditor) { return MakeShared(InPhysicsAssetEditor); } void FPhysicsAssetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) { BindCommands(); DetailLayout.HideCategory(TEXT("Profiles")); PhysicalAnimationProfilesHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UPhysicsAsset, PhysicalAnimationProfiles)); ConstraintProfilesHandle = DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UPhysicsAsset, ConstraintProfiles)); #if !WITH_CHAOS // Hide Chaos-Only settings in PhysX DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(UPhysicsAsset, SolverIterations))->MarkHiddenByCustomization(); #endif DetailLayout.EditCategory(TEXT("Physical Animation Profiles")) .AddProperty(PhysicalAnimationProfilesHandle) .CustomWidget() .WholeRowContent() [ MakePhysicalAnimationProfilesWidget() ]; DetailLayout.EditCategory(TEXT("Constraint Profiles")) .AddProperty(ConstraintProfilesHandle) .CustomWidget() .WholeRowContent() [ MakeConstraintProfilesWidget() ]; } void FPhysicsAssetDetailsCustomization::BindCommands() { const FPhysicsAssetEditorCommands& Commands = FPhysicsAssetEditorCommands::Get(); TSharedPtr CommandList = PhysicsAssetEditorPtr.Pin()->GetToolkitCommands(); CommandList->MapAction( Commands.NewPhysicalAnimationProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::NewPhysicalAnimationProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanCreateNewPhysicalAnimationProfile) ); CommandList->MapAction( Commands.DuplicatePhysicalAnimationProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::DuplicatePhysicalAnimationProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanDuplicatePhysicalAnimationProfile) ); CommandList->MapAction( Commands.DeleteCurrentPhysicalAnimationProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::DeleteCurrentPhysicalAnimationProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanDeleteCurrentPhysicalAnimationProfile) ); CommandList->MapAction( Commands.AddBodyToPhysicalAnimationProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::AddBodyToPhysicalAnimationProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanAddBodyToPhysicalAnimationProfile) ); CommandList->MapAction( Commands.RemoveBodyFromPhysicalAnimationProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::RemoveBodyFromPhysicalAnimationProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanRemoveBodyFromPhysicalAnimationProfile) ); CommandList->MapAction( Commands.NewConstraintProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::NewConstraintProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanCreateNewConstraintProfile) ); CommandList->MapAction( Commands.DuplicateConstraintProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::DuplicateConstraintProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanDuplicateConstraintProfile) ); CommandList->MapAction( Commands.DeleteCurrentConstraintProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::DeleteCurrentConstraintProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanDeleteCurrentConstraintProfile) ); CommandList->MapAction( Commands.AddConstraintToCurrentConstraintProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::AddConstraintToCurrentConstraintProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanAddConstraintToCurrentConstraintProfile) ); CommandList->MapAction( Commands.RemoveConstraintFromCurrentConstraintProfile, FExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::RemoveConstraintFromCurrentConstraintProfile), FCanExecuteAction::CreateSP(this, &FPhysicsAssetDetailsCustomization::CanRemoveConstraintFromCurrentConstraintProfile) ); } TSharedRef< SWidget > FPhysicsAssetDetailsCustomization::FillPhysicalAnimationProfileOptions() { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); TSharedPtr CommandList = PhysicsAssetEditorPtr.Pin()->GetToolkitCommands(); const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, CommandList); const FPhysicsAssetEditorCommands& Commands = FPhysicsAssetEditorCommands::Get(); const float MenuIconSize = FCoreStyle::Get().GetFloat("Menu.MenuIconSize", nullptr, 16.f); if(SharedData->PhysicsAsset) { MenuBuilder.BeginSection("CurrentProfile", LOCTEXT("PhysicsAssetEditor_CurrentPhysicalAnimationMenu", "Current Profile")); { MenuBuilder.AddMenuEntry(Commands.DuplicatePhysicalAnimationProfile); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("PhysicalAnimationProfile", LOCTEXT("PhysicsAssetEditor_PhysicalAnimationMenu", "Physical Animation Profiles")); { TArray ProfileNames; ProfileNames.Add(NAME_None); ProfileNames.Append(SharedData->PhysicsAsset->GetPhysicalAnimationProfileNames()); //Make sure we don't have multiple Nones if user forgot to name profile for(int32 ProfileIdx = ProfileNames.Num()-1; ProfileIdx > 0; --ProfileIdx) { if(ProfileNames[ProfileIdx] == NAME_None) { ProfileNames.RemoveAtSwap(ProfileIdx); } } for(FName ProfileName : ProfileNames) { FUIAction Action; Action.ExecuteAction = FExecuteAction::CreateLambda( [SharedData, ProfileName]() { FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::SetDirectly); //Ensure focus is removed because the menu has already closed and the cached value (the one the user has typed) is going to apply to the new profile SharedData->PhysicsAsset->CurrentPhysicalAnimationProfileName = ProfileName; for(USkeletalBodySetup* BS : SharedData->PhysicsAsset->SkeletalBodySetups) { if(FPhysicalAnimationProfile* Profile = BS->FindPhysicalAnimationProfile(ProfileName)) { BS->CurrentPhysicalAnimationProfile = *Profile; } } }); Action.GetActionCheckState = FGetActionCheckState::CreateLambda([SharedData, ProfileName]() { return SharedData->PhysicsAsset->CurrentPhysicalAnimationProfileName == ProfileName ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }); auto SearchClickedLambda = [ProfileName, SharedData]() { TArray NewBodiesSelection; for (int32 BSIndex = 0; BSIndex < SharedData->PhysicsAsset->SkeletalBodySetups.Num(); ++BSIndex) { const USkeletalBodySetup* BS = SharedData->PhysicsAsset->SkeletalBodySetups[BSIndex]; if (BS->FindPhysicalAnimationProfile(ProfileName)) { NewBodiesSelection.Add(BSIndex); } } SharedData->ClearSelectedBody(); //clear selection SharedData->SetSelectedBodiesAnyPrim(NewBodiesSelection, true); FSlateApplication::Get().DismissAllMenus(); return FReply::Handled(); }; TSharedRef PhysAnimProfileButton = SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(FText::FromString(ProfileName.ToString())) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(2.f, 0.f, 0.f, 0.f) .VAlign(VAlign_Center) [ SNew(SButton) .ButtonStyle(FEditorStyle::Get(), "HoverHintOnly") .ToolTipText(LOCTEXT("SelectBodies", "Select all bodies that are assigned to this profile.")) .OnClicked_Lambda(SearchClickedLambda) [ SNew(SBox) .WidthOverride(MenuIconSize) .HeightOverride(MenuIconSize) .Visibility_Lambda([ProfileName](){ return (ProfileName == NAME_None) ? EVisibility::Collapsed : EVisibility::Visible; }) [ SNew(SImage) .Image(FSlateIcon(FEditorStyle::GetStyleSetName(), "Symbols.SearchGlass").GetIcon()) ] ] ]; //MenuBuilder.AddMenuEntry( FText::FromString(ProfileName.ToString()), FText::GetEmpty(), FSlateIcon(), Action, NAME_None, EUserInterfaceActionType::Check); MenuBuilder.AddMenuEntry(Action, PhysAnimProfileButton, NAME_None, TAttribute(), EUserInterfaceActionType::Check); } } MenuBuilder.EndSection(); } return MenuBuilder.MakeWidget(); } TSharedRef< SWidget > FPhysicsAssetDetailsCustomization::FillConstraintProfilesOptions() { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); TSharedPtr CommandList = PhysicsAssetEditorPtr.Pin()->GetToolkitCommands(); const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, CommandList); const FPhysicsAssetEditorCommands& Commands = FPhysicsAssetEditorCommands::Get(); const float MenuIconSize = FCoreStyle::Get().GetFloat("Menu.MenuIconSize", nullptr, 16.f); if(SharedData->PhysicsAsset) { MenuBuilder.BeginSection("CurrentProfile", LOCTEXT("PhysicsAssetEditor_CurrentProfileMenu", "Current Profile")); { MenuBuilder.AddMenuEntry(Commands.DuplicateConstraintProfile); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("ConstraintProfiles", LOCTEXT("PhysicsAssetEditor_ConstraintProfileMenu", "Constraint Profiles")); { TArray ProfileNames; ProfileNames.Add(NAME_None); ProfileNames.Append(SharedData->PhysicsAsset->GetConstraintProfileNames()); //Make sure we don't have multiple Nones if user forgot to name profile for (int32 ProfileIdx = ProfileNames.Num() - 1; ProfileIdx > 0; --ProfileIdx) { if (ProfileNames[ProfileIdx] == NAME_None) { ProfileNames.RemoveAtSwap(ProfileIdx); } } for (FName ProfileName : ProfileNames) { FUIAction Action; Action.ExecuteAction = FExecuteAction::CreateLambda([SharedData, ProfileName]() { FSlateApplication::Get().ClearKeyboardFocus(EFocusCause::SetDirectly); //Ensure focus is removed because the menu has already closed and the cached value (the one the user has typed) is going to apply to the new profile SharedData->PhysicsAsset->CurrentConstraintProfileName = ProfileName; for (UPhysicsConstraintTemplate* CS : SharedData->PhysicsAsset->ConstraintSetup) { CS->ApplyConstraintProfile(ProfileName, CS->DefaultInstance, /*DefaultIfNotFound=*/ false); //keep settings as they currently are if user wants to add to profile } SharedData->EditorSkelComp->SetConstraintProfileForAll(ProfileName, /*bDefaultIfNotFound=*/ true); }); Action.GetActionCheckState = FGetActionCheckState::CreateLambda([SharedData, ProfileName]() { return SharedData->PhysicsAsset->CurrentConstraintProfileName == ProfileName ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }); auto SearchClickedLambda = [ProfileName, SharedData]() { TArray NewSelectedConstraints; for (int32 CSIndex = 0; CSIndex < SharedData->PhysicsAsset->ConstraintSetup.Num(); ++CSIndex) { const UPhysicsConstraintTemplate* CS = SharedData->PhysicsAsset->ConstraintSetup[CSIndex]; if (CS->ContainsConstraintProfile(ProfileName)) { NewSelectedConstraints.AddUnique(CSIndex); } } SharedData->ClearSelectedConstraints(); //clear selection SharedData->SetSelectedConstraints(NewSelectedConstraints, true); FSlateApplication::Get().DismissAllMenus(); return FReply::Handled(); }; TSharedRef ConstraintProfileButton = SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(FText::FromString(ProfileName.ToString())) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(2.f, 0.f, 0.f, 0.f) .VAlign(VAlign_Center) [ SNew(SButton) .ButtonStyle(FEditorStyle::Get(), "HoverHintOnly") .ToolTipText(LOCTEXT("SelectConstraints", "Select all constraints that are assigned to this profile.")) .OnClicked_Lambda(SearchClickedLambda) [ SNew(SBox) .WidthOverride(MenuIconSize) .HeightOverride(MenuIconSize) .Visibility_Lambda([ProfileName]() { return (ProfileName == NAME_None) ? EVisibility::Collapsed : EVisibility::Visible; }) [ SNew(SImage) .Image(FSlateIcon(FEditorStyle::GetStyleSetName(), "Symbols.SearchGlass").GetIcon()) ] ] ]; MenuBuilder.AddMenuEntry(Action, ConstraintProfileButton, NAME_None, TAttribute(), EUserInterfaceActionType::Check); } } MenuBuilder.EndSection(); } return MenuBuilder.MakeWidget(); } void FPhysicsAssetDetailsCustomization::HandlePhysicalAnimationProfileNameCommitted(const FText& InText, ETextCommit::Type InCommitType) { PhysicalAnimationProfileNameTextBox->SetError(FText::GetEmpty()); if(InCommitType != ETextCommit::OnCleared) { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); int32 PhysicalAnimationProfileIndex = INDEX_NONE; SharedData->PhysicsAsset->PhysicalAnimationProfiles.Find(SharedData->PhysicsAsset->CurrentPhysicalAnimationProfileName, PhysicalAnimationProfileIndex); if(PhysicalAnimationProfileIndex != INDEX_NONE) { FName NewName = *InText.ToString(); if(!SharedData->PhysicsAsset->GetPhysicalAnimationProfileNames().Contains(NewName)) { TSharedPtr ChildHandle = PhysicalAnimationProfilesHandle->GetChildHandle(PhysicalAnimationProfileIndex); const FScopedTransaction Transaction(LOCTEXT("RenamePhysicalAnimationProfile", "Rename Physical Animation Profile")); const FName OldProfileName = SharedData->PhysicsAsset->CurrentPhysicalAnimationProfileName; SharedData->PhysicsAsset->Modify(); SharedData->PhysicsAsset->CurrentPhysicalAnimationProfileName = NewName; ChildHandle->SetValue( SharedData->PhysicsAsset->CurrentPhysicalAnimationProfileName); } } } } void FPhysicsAssetDetailsCustomization::HandleConstraintProfileNameCommitted(const FText& InText, ETextCommit::Type InCommitType) { ConstraintProfileNameTextBox->SetError(FText::GetEmpty()); if(InCommitType != ETextCommit::OnCleared) { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); int32 ConstraintProfileIndex = INDEX_NONE; SharedData->PhysicsAsset->ConstraintProfiles.Find(SharedData->PhysicsAsset->CurrentConstraintProfileName, ConstraintProfileIndex); if(ConstraintProfileIndex != INDEX_NONE) { FName NewName = *InText.ToString(); if(!SharedData->PhysicsAsset->GetConstraintProfileNames().Contains(NewName)) { TSharedPtr ChildHandle = ConstraintProfilesHandle->GetChildHandle(ConstraintProfileIndex); const FScopedTransaction Transaction(LOCTEXT("RenameConstraintProfile", "Rename Constraint Profile")); const FName OldProfileName = SharedData->PhysicsAsset->CurrentConstraintProfileName; SharedData->PhysicsAsset->Modify(); SharedData->PhysicsAsset->CurrentConstraintProfileName = NewName; ChildHandle->SetValue(SharedData->PhysicsAsset->CurrentConstraintProfileName); } } } } TSharedRef FPhysicsAssetDetailsCustomization::CreateProfileButton(const FText& InGlyph, TSharedPtr InCommand) { check(InCommand.IsValid()); TWeakPtr LocalCommandPtr = InCommand; return SNew(SButton) .VAlign(EVerticalAlignment::VAlign_Center) .ButtonStyle(FEditorStyle::Get(), "FlatButton") .ForegroundColor(FEditorStyle::GetSlateColor("DefaultForeground")) .ToolTipText(InCommand->GetDescription()) .IsEnabled_Lambda([this, LocalCommandPtr]() { TSharedPtr CommandList = PhysicsAssetEditorPtr.Pin()->GetToolkitCommands(); return CommandList->CanExecuteAction(LocalCommandPtr.Pin().ToSharedRef()); }) .OnClicked(FOnClicked::CreateLambda([this, LocalCommandPtr]() { TSharedPtr CommandList = PhysicsAssetEditorPtr.Pin()->GetToolkitCommands(); return CommandList->ExecuteAction(LocalCommandPtr.Pin().ToSharedRef()) ? FReply::Handled() : FReply::Unhandled(); })) [ SNew( SHorizontalBox ) +SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(STextBlock) .TextStyle(FEditorStyle::Get(), "PhysicsAssetEditor.Profiles.Font") .Font(FEditorStyle::Get().GetFontStyle("FontAwesome.11")) .Text(InGlyph) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4, 0, 0, 0) [ SNew(STextBlock) .TextStyle( FEditorStyle::Get(), "PhysicsAssetEditor.Profiles.Font" ) .Text(InCommand->GetLabel()) ] ]; } TSharedRef FPhysicsAssetDetailsCustomization::MakePhysicalAnimationProfilesWidget() { const FPhysicsAssetEditorCommands& Commands = FPhysicsAssetEditorCommands::Get(); TWeakPtr LocalPhysicsAssetEditorPtr = PhysicsAssetEditorPtr; return SNew(SHorizontalBox) .ToolTipText(LOCTEXT("CurrentPhysicalAnimationProfileWidgetTooltip", "Select and edit the current physical animation profile.")) +SHorizontalBox::Slot() .FillWidth(1.0f) .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ SNew(SComboButton) .OnGetMenuContent(this, &FPhysicsAssetDetailsCustomization::FillPhysicalAnimationProfileOptions) .ButtonContent() [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 0.0f, 2.0f, 3.0f) [ SNew(STextBlock) .Text(LOCTEXT("CurrentProfile", "Current Profile")) ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f, 0.0f, 2.0f, 0.0f) [ SAssignNew(PhysicalAnimationProfileNameTextBox, SEditableTextBox) .Text_Lambda([LocalPhysicsAssetEditorPtr]() { return FText::FromName(LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->CurrentPhysicalAnimationProfileName); }) .IsEnabled_Lambda([LocalPhysicsAssetEditorPtr]() { return LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->CurrentPhysicalAnimationProfileName != NAME_None; }) .OnTextChanged_Lambda([this, LocalPhysicsAssetEditorPtr](const FText& InText) { FName ProfileAsName = *InText.ToString(); if(LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->CurrentPhysicalAnimationProfileName != ProfileAsName && LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->GetPhysicalAnimationProfileNames().Contains(ProfileAsName)) { PhysicalAnimationProfileNameTextBox->SetError(FText::Format(LOCTEXT("ProfileExists", "Profile '{0}' already exists"), InText)); } else { PhysicalAnimationProfileNameTextBox->SetError(FText::GetEmpty()); } }) .OnTextCommitted(FOnTextCommitted::CreateSP(this, &FPhysicsAssetDetailsCustomization::HandlePhysicalAnimationProfileNameCommitted)) ] ] ] ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SUniformGridPanel) .SlotPadding(FMargin(1.0f, 1.0f)) +SUniformGridPanel::Slot(0, 0) [ CreateProfileButton(FEditorFontGlyphs::File, Commands.NewPhysicalAnimationProfile) ] +SUniformGridPanel::Slot(1, 0) [ CreateProfileButton(FEditorFontGlyphs::Trash, Commands.DeleteCurrentPhysicalAnimationProfile) ] +SUniformGridPanel::Slot(0, 1) [ CreateProfileButton(FEditorFontGlyphs::Plus_Circle, Commands.AddBodyToPhysicalAnimationProfile) ] +SUniformGridPanel::Slot(1, 1) [ CreateProfileButton(FEditorFontGlyphs::Minus_Circle, Commands.RemoveBodyFromPhysicalAnimationProfile) ] ]; } TSharedRef FPhysicsAssetDetailsCustomization::MakeConstraintProfilesWidget() { const FPhysicsAssetEditorCommands& Commands = FPhysicsAssetEditorCommands::Get(); TWeakPtr LocalPhysicsAssetEditorPtr = PhysicsAssetEditorPtr; return SNew(SHorizontalBox) .ToolTipText(LOCTEXT("CurrentConstraintProfileWidgetTooltip", "Select and edit the current constraint profile.")) +SHorizontalBox::Slot() .FillWidth(1.0f) .HAlign(HAlign_Left) .VAlign(VAlign_Center) [ SNew(SComboButton) .OnGetMenuContent(this, &FPhysicsAssetDetailsCustomization::FillConstraintProfilesOptions) .ButtonContent() [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 0.0f, 2.0f, 3.0f) [ SNew(STextBlock) .Text(LOCTEXT("CurrentProfile", "Current Profile")) ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f, 0.0f, 2.0f, 0.0f) [ SAssignNew(ConstraintProfileNameTextBox, SEditableTextBox) .Text_Lambda([LocalPhysicsAssetEditorPtr]() { return FText::FromName(LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->CurrentConstraintProfileName); }) .IsEnabled_Lambda([LocalPhysicsAssetEditorPtr]() { return LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->CurrentConstraintProfileName != NAME_None; }) .OnTextChanged_Lambda([this, LocalPhysicsAssetEditorPtr](const FText& InText) { FName ProfileAsName = *InText.ToString(); if(LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->CurrentConstraintProfileName != ProfileAsName && LocalPhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset->GetConstraintProfileNames().Contains(ProfileAsName)) { ConstraintProfileNameTextBox->SetError(FText::Format(LOCTEXT("ProfileExists", "Profile '{0}' already exists"), InText)); } else { ConstraintProfileNameTextBox->SetError(FText::GetEmpty()); } }) .OnTextCommitted(FOnTextCommitted::CreateSP(this, &FPhysicsAssetDetailsCustomization::HandleConstraintProfileNameCommitted)) ] ] ] ] +SHorizontalBox::Slot() .AutoWidth() [ SNew(SUniformGridPanel) .SlotPadding(FMargin(1.0f, 1.0f)) +SUniformGridPanel::Slot(0, 0) [ CreateProfileButton(FEditorFontGlyphs::File, Commands.NewConstraintProfile) ] +SUniformGridPanel::Slot(1, 0) [ CreateProfileButton(FEditorFontGlyphs::Trash, Commands.DeleteCurrentConstraintProfile) ] +SUniformGridPanel::Slot(0, 1) [ CreateProfileButton(FEditorFontGlyphs::Plus_Circle, Commands.AddConstraintToCurrentConstraintProfile) ] +SUniformGridPanel::Slot(1, 1) [ CreateProfileButton(FEditorFontGlyphs::Minus_Circle, Commands.RemoveConstraintFromCurrentConstraintProfile) ] ]; } void FPhysicsAssetDetailsCustomization::ApplyPhysicalAnimationProfile(FName InName) { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; SharedData->PhysicsAsset->CurrentPhysicalAnimationProfileName = InName; for(USkeletalBodySetup* BodySetup : SharedData->PhysicsAsset->SkeletalBodySetups) { if(FPhysicalAnimationProfile* Profile = BodySetup->FindPhysicalAnimationProfile(InName)) { BodySetup->CurrentPhysicalAnimationProfile = *Profile; } } } void FPhysicsAssetDetailsCustomization::NewPhysicalAnimationProfile() { const FScopedTransaction Transaction(LOCTEXT("AddPhysicalAnimationProfile", "Add Physical Animation Profile")); TSharedPtr ArrayHandle = PhysicalAnimationProfilesHandle->AsArray(); ArrayHandle->AddItem(); // now apply the new profile TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); FName ProfileName = SharedData->PhysicsAsset->PhysicalAnimationProfiles.Last(); ApplyPhysicalAnimationProfile(ProfileName); } bool FPhysicsAssetDetailsCustomization::CanCreateNewPhysicalAnimationProfile() const { return PhysicsAssetEditorPtr.Pin()->IsNotSimulation(); } void FPhysicsAssetDetailsCustomization::DuplicatePhysicalAnimationProfile() { int32 PhysicalAnimationProfileIndex = INDEX_NONE; TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; PhysicsAsset->PhysicalAnimationProfiles.Find(PhysicsAsset->CurrentPhysicalAnimationProfileName, PhysicalAnimationProfileIndex); if(PhysicalAnimationProfileIndex != INDEX_NONE) { const FScopedTransaction Transaction(LOCTEXT("DuplicatePhysicalAnimationProfile", "Duplicate Physical Animation Profile")); TSharedPtr ArrayHandle = PhysicalAnimationProfilesHandle->AsArray(); ArrayHandle->DuplicateItem(PhysicalAnimationProfileIndex); // now apply the new profile FName ProfileName = PhysicsAsset->PhysicalAnimationProfiles[PhysicalAnimationProfileIndex]; ApplyPhysicalAnimationProfile(ProfileName); } } bool FPhysicsAssetDetailsCustomization::CanDuplicatePhysicalAnimationProfile() const { UPhysicsAsset* PhysicsAsset = PhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset; return PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && PhysicsAsset->CurrentPhysicalAnimationProfileName != NAME_None; } void FPhysicsAssetDetailsCustomization::DeleteCurrentPhysicalAnimationProfile() { int32 PhysicalAnimationProfileIndex = INDEX_NONE; UPhysicsAsset* PhysicsAsset = PhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset; PhysicsAsset->PhysicalAnimationProfiles.Find(PhysicsAsset->CurrentPhysicalAnimationProfileName, PhysicalAnimationProfileIndex); if(PhysicalAnimationProfileIndex != INDEX_NONE) { const FScopedTransaction Transaction(LOCTEXT("DeletePhysicalAnimationProfile", "Delete Physical Animation Profile")); PhysicalAnimationProfilesHandle->AsArray()->DeleteItem(PhysicalAnimationProfileIndex); ApplyPhysicalAnimationProfile(NAME_None); } } bool FPhysicsAssetDetailsCustomization::CanDeleteCurrentPhysicalAnimationProfile() const { UPhysicsAsset* PhysicsAsset = PhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset; return PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && PhysicsAsset->CurrentPhysicalAnimationProfileName != NAME_None; } void FPhysicsAssetDetailsCustomization::AddBodyToPhysicalAnimationProfile() { const FScopedTransaction Transaction(LOCTEXT("AssignToPhysicalAnimationProfile", "Assign To Physical Animation Profile")); TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; for(int32 BodySetupIndex = 0; BodySetupIndex < SharedData->SelectedBodies.Num(); ++BodySetupIndex) { USkeletalBodySetup* BodySetup = PhysicsAsset->SkeletalBodySetups[SharedData->SelectedBodies[BodySetupIndex].Index]; if(BodySetup) { BodySetup->Modify(); FName ProfileName = BodySetup->GetCurrentPhysicalAnimationProfileName(); if (!BodySetup->FindPhysicalAnimationProfile(ProfileName)) { BodySetup->CurrentPhysicalAnimationProfile = FPhysicalAnimationProfile(); BodySetup->AddPhysicalAnimationProfile(ProfileName); } } } } bool FPhysicsAssetDetailsCustomization::CanAddBodyToPhysicalAnimationProfile() const { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); TWeakPtr WeakSharedData = SharedData; UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; auto PhysicalAnimationProfileExistsForAll = [WeakSharedData]() { TSharedPtr LocalSharedData = WeakSharedData.Pin(); for(int32 BodySetupIndex = 0; BodySetupIndex < LocalSharedData->SelectedBodies.Num(); ++BodySetupIndex) { USkeletalBodySetup* BodySetup = LocalSharedData->PhysicsAsset->SkeletalBodySetups[LocalSharedData->SelectedBodies[BodySetupIndex].Index]; if(BodySetup) { if (!BodySetup->FindPhysicalAnimationProfile(BodySetup->GetCurrentPhysicalAnimationProfileName())) { return false; } } else { return false; } } return true; }; const bool bSelectedBodies = SharedData->SelectedBodies.Num() > 0; return (PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && bSelectedBodies && !PhysicalAnimationProfileExistsForAll() && PhysicsAsset->CurrentPhysicalAnimationProfileName != NAME_None); } void FPhysicsAssetDetailsCustomization::RemoveBodyFromPhysicalAnimationProfile() { const FScopedTransaction Transaction(LOCTEXT("UnassignFromPhysicalAnimationProfile", "Unassign From Physical Animation Profile")); TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; for(int32 BodySetupIndex = 0; BodySetupIndex < SharedData->SelectedBodies.Num(); ++BodySetupIndex) { USkeletalBodySetup* BodySetup = SharedData->PhysicsAsset->SkeletalBodySetups[SharedData->SelectedBodies[BodySetupIndex].Index]; if(BodySetup) { FName ProfileName = BodySetup->GetCurrentPhysicalAnimationProfileName(); BodySetup->RemovePhysicalAnimationProfile(ProfileName); } } } bool FPhysicsAssetDetailsCustomization::CanRemoveBodyFromPhysicalAnimationProfile() const { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); TWeakPtr WeakSharedData = SharedData; UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; auto PhysicalAnimationProfileExistsForAny = [WeakSharedData]() { TSharedPtr LocalSharedData = WeakSharedData.Pin(); for(int32 BodySetupIndex = 0; BodySetupIndex < LocalSharedData->SelectedBodies.Num(); ++BodySetupIndex) { USkeletalBodySetup* BodySetup = LocalSharedData->PhysicsAsset->SkeletalBodySetups[LocalSharedData->SelectedBodies[BodySetupIndex].Index]; if(BodySetup && BodySetup->FindPhysicalAnimationProfile(BodySetup->GetCurrentPhysicalAnimationProfileName())) { return true; } } return false; }; const bool bSelectedBodies = SharedData->SelectedBodies.Num() > 0; return (PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && bSelectedBodies && PhysicalAnimationProfileExistsForAny() && PhysicsAsset->CurrentPhysicalAnimationProfileName != NAME_None); } void FPhysicsAssetDetailsCustomization::ApplyConstraintProfile(FName InName) { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); SharedData->PhysicsAsset->CurrentConstraintProfileName = InName; for (UPhysicsConstraintTemplate* CS : SharedData->PhysicsAsset->ConstraintSetup) { CS->ApplyConstraintProfile(InName, CS->DefaultInstance, /*DefaultIfNotFound=*/ false); //keep settings as they currently are if user wants to add to profile } SharedData->EditorSkelComp->SetConstraintProfileForAll(InName, /*bDefaultIfNotFound=*/ true); } bool FPhysicsAssetDetailsCustomization::ConstraintProfileExistsForAny() const { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); const FName ProfileName = SharedData->PhysicsAsset->CurrentConstraintProfileName; for(int32 ConstraintIndex = 0; ConstraintIndex < SharedData->SelectedConstraints.Num(); ++ConstraintIndex) { UPhysicsConstraintTemplate* ConstraintSetup = SharedData->PhysicsAsset->ConstraintSetup[SharedData->SelectedConstraints[ConstraintIndex].Index]; if(ConstraintSetup && ConstraintSetup->ContainsConstraintProfile(ProfileName)) { return true; } } return false; } void FPhysicsAssetDetailsCustomization::NewConstraintProfile() { const FScopedTransaction Transaction(LOCTEXT("AddConstraintProfile", "Add Constraint Profile")); TSharedPtr ArrayHandle = ConstraintProfilesHandle->AsArray(); ArrayHandle->AddItem(); // now apply the new profile TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); FName ProfileName = SharedData->PhysicsAsset->ConstraintProfiles.Last(); ApplyConstraintProfile(ProfileName); } bool FPhysicsAssetDetailsCustomization::CanCreateNewConstraintProfile() const { return PhysicsAssetEditorPtr.Pin()->IsNotSimulation(); } void FPhysicsAssetDetailsCustomization::DuplicateConstraintProfile() { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; int32 ConstraintProfileIndex = INDEX_NONE; PhysicsAsset->ConstraintProfiles.Find(PhysicsAsset->CurrentConstraintProfileName, ConstraintProfileIndex); if(ConstraintProfileIndex != INDEX_NONE) { const FScopedTransaction Transaction(LOCTEXT("DuplicateConstraintProfile", "Duplicate Constraint Profile")); TSharedPtr ArrayHandle = ConstraintProfilesHandle->AsArray(); ArrayHandle->DuplicateItem(ConstraintProfileIndex); // now apply the new profile FName ProfileName = PhysicsAsset->ConstraintProfiles[ConstraintProfileIndex]; ApplyConstraintProfile(ProfileName); } } bool FPhysicsAssetDetailsCustomization::CanDuplicateConstraintProfile() const { UPhysicsAsset* PhysicsAsset = PhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset; return PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && PhysicsAsset->CurrentConstraintProfileName != NAME_None; } void FPhysicsAssetDetailsCustomization::DeleteCurrentConstraintProfile() { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); int32 ConstraintProfileIndex = INDEX_NONE; SharedData->PhysicsAsset->ConstraintProfiles.Find(SharedData->PhysicsAsset->CurrentConstraintProfileName, ConstraintProfileIndex); if(ConstraintProfileIndex != INDEX_NONE) { const FScopedTransaction Transaction(LOCTEXT("DeleteConstraintProfile", "Delete Constraint Profile")); ConstraintProfilesHandle->AsArray()->DeleteItem(ConstraintProfileIndex); ApplyConstraintProfile(NAME_None); } } bool FPhysicsAssetDetailsCustomization::CanDeleteCurrentConstraintProfile() const { UPhysicsAsset* PhysicsAsset = PhysicsAssetEditorPtr.Pin()->GetSharedData()->PhysicsAsset; return PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && PhysicsAsset->CurrentConstraintProfileName != NAME_None; } void FPhysicsAssetDetailsCustomization::AddConstraintToCurrentConstraintProfile() { const FScopedTransaction Transaction(LOCTEXT("AssignToConstraintProfile", "Assign To Constraint Profile")); TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); for(int32 ConstraintIndex = 0; ConstraintIndex < SharedData->SelectedConstraints.Num(); ++ConstraintIndex) { UPhysicsConstraintTemplate* ConstraintSetup = SharedData->PhysicsAsset->ConstraintSetup[SharedData->SelectedConstraints[ConstraintIndex].Index]; FName ProfileName = ConstraintSetup->GetCurrentConstraintProfileName(); if (!ConstraintSetup->ContainsConstraintProfile(ProfileName)) { ConstraintSetup->Modify(); ConstraintSetup->AddConstraintProfile(ProfileName); } } } bool FPhysicsAssetDetailsCustomization::CanAddConstraintToCurrentConstraintProfile() const { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); TWeakPtr WeakSharedData = SharedData; UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; auto ConstraintProfileExistsForAll = [WeakSharedData]() { TSharedPtr LocalSharedData = WeakSharedData.Pin(); const FName ProfileName = LocalSharedData->PhysicsAsset->CurrentConstraintProfileName; for(int32 ConstraintIndex = 0; ConstraintIndex < LocalSharedData->SelectedConstraints.Num(); ++ConstraintIndex) { UPhysicsConstraintTemplate* ConstraintSetup = LocalSharedData->PhysicsAsset->ConstraintSetup[LocalSharedData->SelectedConstraints[ConstraintIndex].Index]; if(ConstraintSetup) { if(!ConstraintSetup->ContainsConstraintProfile(ProfileName)) { return false; } } else { return false; } } return true; }; const bool bSelectedConstraints = SharedData->SelectedConstraints.Num() > 0; return (PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && bSelectedConstraints && PhysicsAsset->CurrentConstraintProfileName != NAME_None && !ConstraintProfileExistsForAll()); } void FPhysicsAssetDetailsCustomization::RemoveConstraintFromCurrentConstraintProfile() { const FScopedTransaction Transaction(LOCTEXT("UnassignFromConstraintProfile", "Unassign From Constraint Profile")); TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); for(int32 ConstraintIndex = 0; ConstraintIndex < SharedData->SelectedConstraints.Num(); ++ConstraintIndex) { UPhysicsConstraintTemplate* ConstraintSetup = SharedData->PhysicsAsset->ConstraintSetup[SharedData->SelectedConstraints[ConstraintIndex].Index]; ConstraintSetup->Modify(); FName ProfileName = ConstraintSetup->GetCurrentConstraintProfileName(); ConstraintSetup->RemoveConstraintProfile(ProfileName); } } bool FPhysicsAssetDetailsCustomization::CanRemoveConstraintFromCurrentConstraintProfile() const { TSharedPtr SharedData = PhysicsAssetEditorPtr.Pin()->GetSharedData(); TWeakPtr WeakSharedData = SharedData; UPhysicsAsset* PhysicsAsset = SharedData->PhysicsAsset; auto ConstraintProfileExistsForAny = [WeakSharedData]() { TSharedPtr LocalSharedData = WeakSharedData.Pin(); const FName ProfileName = LocalSharedData->PhysicsAsset->CurrentConstraintProfileName; for(int32 ConstraintIndex = 0; ConstraintIndex < LocalSharedData->SelectedConstraints.Num(); ++ConstraintIndex) { UPhysicsConstraintTemplate* ConstraintSetup = LocalSharedData->PhysicsAsset->ConstraintSetup[LocalSharedData->SelectedConstraints[ConstraintIndex].Index]; if(ConstraintSetup && ConstraintSetup->ContainsConstraintProfile(ProfileName)) { return true; } } return false; }; const bool bSelectedConstraints = SharedData->SelectedConstraints.Num() > 0; return (PhysicsAssetEditorPtr.Pin()->IsNotSimulation() && bSelectedConstraints && PhysicsAsset->CurrentConstraintProfileName != NAME_None && ConstraintProfileExistsForAny()); } #undef LOCTEXT_NAMESPACE